AI绘图程序开发
首先利用Hermes agent输入要求: 你是一个资深的程序专家,写一段Python程序,主要功能及用途如下:1,采用对话框界面,2,根据API和网址以及选定的模型名称调用模型进行绘图
展示 软件界面:

绘制成果:

第一版代码如下:
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""AI 图像生成工具 (AI Image Generator)=====================================功能:通过 GUI 对话框界面,调用支持 OpenAI 兼容 API 的图像生成模型进行绘图。支持平台:OpenAI DALL-E、Stability AI、硅基流动(SiliconFlow)、通义万相、智谱 CogView 等。依赖安装: pip install tkinter (通常 Python 自带) pip install requests pip install Pillow使用方法: python ai_image_generator.py"""import tkinter as tkfrom tkinter import ttk, messagebox, filedialog, scrolledtextimport requestsimport jsonimport base64import osimport sysimport threadingfrom datetime import datetimefrom pathlib import Pathfrom io import BytesIOtry:from PIL import Image, ImageTkexcept ImportError:print("❌ 缺少 Pillow 库,请执行:pip install Pillow") sys.exit(1)try:import requestsexcept ImportError:print("❌ 缺少 requests 库,请执行:pip install requests") sys.exit(1)# ============================================================# 预设模型配置# ============================================================PRESET_MODELS = {"OpenAI DALL-E 3": {"api_base": "https://api.openai.com/v1","model": "dall-e-3","endpoint": "/images/generations","sizes": ["1024x1024", "1024x1792", "1792x1024"],"default_size": "1024x1024","style_options": ["vivid", "natural"],"quality_options": ["standard", "hd"],"auth_type": "bearer","note": "需要 OpenAI API Key,支持 DALL-E 3", },"OpenAI DALL-E 2": {"api_base": "https://api.openai.com/v1","model": "dall-e-2","endpoint": "/images/generations","sizes": ["256x256", "512x512", "1024x1024"],"default_size": "1024x1024","style_options": None,"quality_options": None,"auth_type": "bearer","note": "需要 OpenAI API Key", },"硅基流动 SiliconFlow": {"api_base": "https://api.siliconflow.cn/v1","model": "stabilityai/stable-diffusion-3-medium","endpoint": "/images/generations","sizes": ["1024x1024", "1024x768", "768x1024", "512x512"],"default_size": "1024x1024","style_options": None,"quality_options": None,"auth_type": "bearer","note": "国内可用,支持多种开源模型", },"智谱 CogView-3": {"api_base": "https://open.bigmodel.cn/api/paas/v4","model": "cogview-3-plus","endpoint": "/images/generations","sizes": ["1024x1024", "768x1344", "864x1152", "1344x768", "1152x864", "1440x720", "720x1440"],"default_size": "1024x1024","style_options": None,"quality_options": None,"auth_type": "bearer","note": "智谱清言出品,国内可用", },"通义万相": {"api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1","model": "wanx-v1","endpoint": "/images/generations","sizes": ["1024*1024", "720*1280", "1280*720"],"default_size": "1024*1024","style_options": None,"quality_options": None,"auth_type": "bearer","note": "阿里云通义万相,国内可用", },"自定义 OpenAI 兼容": {"api_base": "","model": "","endpoint": "/images/generations","sizes": ["1024x1024", "512x512", "256x256"],"default_size": "1024x1024","style_options": None,"quality_options": None,"auth_type": "bearer","note": "适用于任何 OpenAI 兼容的图像生成 API", },}# ============================================================# 配置持久化# ============================================================CONFIG_FILE = Path.home() / ".ai_image_generator_config.json"defload_config():"""加载保存的配置"""if CONFIG_FILE.exists():try:withopen(CONFIG_FILE, "r", encoding="utf-8") as f:return json.load(f)except Exception:passreturn {}defsave_config(config):"""保存配置到文件"""try:withopen(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2)except Exception:pass# ============================================================# 主应用程序# ============================================================classAIImageGenerator:"""AI 图像生成器主窗口"""def__init__(self, root: tk.Tk):self.root = rootself.root.title("🎨 AI 图像生成工具")self.root.geometry("960x800")self.root.minsize(800, 650)# 状态变量self.generated_images: list[Image.Image] = []self.current_image_index = 0self.is_generating = Falseself.api_key = tk.StringVar()self.api_base = tk.StringVar()self.model_name = tk.StringVar()self.image_size = tk.StringVar(value="1024x1024")self.image_style = tk.StringVar(value="vivid")self.image_quality = tk.StringVar(value="standard")self.n_var = tk.IntVar(value=1)self.prompt_text = tk.StringVar()# 加载配置 saved = load_config()self.api_key.set(saved.get("api_key", ""))self.api_base.set(saved.get("api_base", ""))self.model_name.set(saved.get("model_name", ""))# 默认输出目录self.output_dir = Path.home() / "AI_Generated_Images"self.output_dir.mkdir(exist_ok=True)# 构建界面self._build_styles()self._build_ui()# 默认选择self.combo_preset.current(0)self.combo_preset.event_generate("<<ComboboxSelected>>")# ----------------------------------------------------------# 样式# ----------------------------------------------------------def_build_styles(self): style = ttk.Style()try: style.theme_use("clam")except Exception:pass style.configure("Title.TLabel", font=("Microsoft YaHei UI", 14, "bold")) style.configure("Section.TLabel", font=("Microsoft YaHei UI", 10, "bold")) style.configure("TButton", font=("Microsoft YaHei UI", 10), padding=6) style.configure("Generate.TButton", font=("Microsoft YaHei UI", 11, "bold"), padding=8) style.configure("TLabelframe.Label", font=("Microsoft YaHei UI", 10, "bold")) style.configure("Status.TLabel", font=("Microsoft YaHei UI", 9), foreground="gray")# ----------------------------------------------------------# 界面构建# ----------------------------------------------------------def_build_ui(self):# 顶层容器(可滚动) outer = ttk.Frame(self.root, padding=10) outer.pack(fill=tk.BOTH, expand=True)# ---- 标题 ---- ttk.Label(outer, text="🎨 AI 图像生成工具", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 10))# ---- 使用 PanedWindow 上下分割 ---- paned = ttk.PanedWindow(outer, orient=tk.VERTICAL) paned.pack(fill=tk.BOTH, expand=True)# ============ 上半部分:设置区 ============ top_frame = ttk.Frame(paned) paned.add(top_frame, weight=1)# --- 预设选择 --- preset_frame = ttk.LabelFrame(top_frame, text="预设模型", padding=8) preset_frame.pack(fill=tk.X, pady=(0, 6))self.combo_preset = ttk.Combobox( preset_frame, values=list(PRESET_MODELS.keys()), state="readonly", width=35, font=("Microsoft YaHei UI", 10), )self.combo_preset.pack(side=tk.LEFT, padx=(0, 10))self.combo_preset.bind("<<ComboboxSelected>>", self._on_preset_changed)self.lbl_note = ttk.Label(preset_frame, text="", style="Status.TLabel", wraplength=500)self.lbl_note.pack(side=tk.LEFT, fill=tk.X, expand=True)# --- API 设置 --- api_frame = ttk.LabelFrame(top_frame, text="API 设置", padding=8) api_frame.pack(fill=tk.X, pady=(0, 6)) ttk.Label(api_frame, text="API 网址:").grid(row=0, column=0, sticky=tk.W, pady=2)self.entry_api_base = ttk.Entry(api_frame, textvariable=self.api_base, width=70, font=("Consolas", 10))self.entry_api_base.grid(row=0, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=2) ttk.Label(api_frame, text="API Key:").grid(row=1, column=0, sticky=tk.W, pady=2)self.entry_api_key = ttk.Entry(api_frame, textvariable=self.api_key, width=70, show="●", font=("Consolas", 10))self.entry_api_key.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=2)self.btn_show_key = ttk.Button(api_frame, text="👁", width=3, command=self._toggle_key_visibility)self.btn_show_key.grid(row=1, column=2, padx=2, pady=2) ttk.Label(api_frame, text="模型名称:").grid(row=2, column=0, sticky=tk.W, pady=2)self.entry_model = ttk.Entry(api_frame, textvariable=self.model_name, width=70, font=("Consolas", 10))self.entry_model.grid(row=2, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=2) api_frame.columnconfigure(1, weight=1)# --- 参数设置 --- param_frame = ttk.LabelFrame(top_frame, text="生成参数", padding=8) param_frame.pack(fill=tk.X, pady=(0, 6))# 第一行:尺寸 ttk.Label(param_frame, text="图片尺寸:").grid(row=0, column=0, sticky=tk.W, pady=2)self.combo_size = ttk.Combobox(param_frame, textvariable=self.image_size, state="readonly", width=15, font=("Consolas", 10))self.combo_size.grid(row=0, column=1, padx=5, pady=2)# 数量 ttk.Label(param_frame, text="生成数量:").grid(row=0, column=2, sticky=tk.W, padx=(20, 0), pady=2)self.spin_n = ttk.Spinbox(param_frame, from_=1, to=4, textvariable=self.n_var, width=5, font=("Consolas", 10))self.spin_n.grid(row=0, column=3, padx=5, pady=2)# 第二行:风格 & 质量(可能隐藏)self.lbl_style = ttk.Label(param_frame, text="风格:")self.lbl_style.grid(row=1, column=0, sticky=tk.W, pady=2)self.combo_style = ttk.Combobox(param_frame, textvariable=self.image_style, state="readonly", width=15, font=("Consolas", 10))self.combo_style.grid(row=1, column=1, padx=5, pady=2)self.lbl_quality = ttk.Label(param_frame, text="质量:")self.lbl_quality.grid(row=1, column=2, sticky=tk.W, padx=(20, 0), pady=2)self.combo_quality = ttk.Combobox(param_frame, textvariable=self.image_quality, state="readonly", width=15, font=("Consolas", 10))self.combo_quality.grid(row=1, column=3, padx=5, pady=2)# --- Prompt --- prompt_frame = ttk.LabelFrame(top_frame, text="绘图描述 (Prompt)", padding=8) prompt_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 6))self.txt_prompt = scrolledtext.ScrolledText( prompt_frame, height=4, font=("Microsoft YaHei UI", 10), wrap=tk.WORD )self.txt_prompt.pack(fill=tk.BOTH, expand=True)# --- 按钮行 --- btn_frame = ttk.Frame(top_frame) btn_frame.pack(fill=tk.X, pady=(0, 4))self.btn_generate = ttk.Button( btn_frame, text="✨ 开始生成", style="Generate.TButton", command=self._on_generate )self.btn_generate.pack(side=tk.LEFT, padx=(0, 10))self.btn_save = ttk.Button(btn_frame, text="💾 保存图片", command=self._on_save, state=tk.DISABLED)self.btn_save.pack(side=tk.LEFT, padx=(0, 10))self.btn_save_all = ttk.Button(btn_frame, text="📁 保存全部", command=self._on_save_all, state=tk.DISABLED)self.btn_save_all.pack(side=tk.LEFT, padx=(0, 10))self.btn_prev = ttk.Button(btn_frame, text="◀", width=3, command=self._on_prev_image, state=tk.DISABLED)self.btn_prev.pack(side=tk.RIGHT, padx=2)self.btn_next = ttk.Button(btn_frame, text="▶", width=3, command=self._on_next_image, state=tk.DISABLED)self.btn_next.pack(side=tk.RIGHT, padx=2)self.lbl_nav = ttk.Label(btn_frame, text="", style="Status.TLabel")self.lbl_nav.pack(side=tk.RIGHT, padx=10)# ============ 下半部分:图片预览 & 日志 ============ bottom_frame = ttk.Frame(paned) paned.add(bottom_frame, weight=1)# 预览 preview_frame = ttk.LabelFrame(bottom_frame, text="图片预览", padding=5) preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 4))self.canvas = tk.Canvas(preview_frame, bg="#f0f0f0", highlightthickness=0)self.canvas.pack(fill=tk.BOTH, expand=True)# 状态栏self.status_var = tk.StringVar(value="就绪 — 选择预设模型或填写 API 信息后开始生成")self.lbl_status = ttk.Label(bottom_frame, textvariable=self.status_var, style="Status.TLabel", anchor=tk.W)self.lbl_status.pack(fill=tk.X)# 绑定关闭事件self.root.protocol("WM_DELETE_WINDOW", self._on_close)self.root.bind("<Control-s>", lambda e: self._on_save())self.root.bind("<Control-Return>", lambda e: self._on_generate())# ----------------------------------------------------------# 预设切换# ----------------------------------------------------------def_on_preset_changed(self, event=None): name = self.combo_preset.get()if name notin PRESET_MODELS:return cfg = PRESET_MODELS[name]self.api_base.set(cfg["api_base"])self.model_name.set(cfg["model"])self.lbl_note.config(text=cfg.get("note", ""))# 尺寸self.combo_size["values"] = cfg["sizes"]self.image_size.set(cfg["default_size"])# 风格if cfg.get("style_options"):self.combo_style["values"] = cfg["style_options"]self.image_style.set(cfg["style_options"][^0])self.lbl_style.grid()self.combo_style.grid()else:self.lbl_style.grid_remove()self.combo_style.grid_remove()# 质量if cfg.get("quality_options"):self.combo_quality["values"] = cfg["quality_options"]self.image_quality.set(cfg["quality_options"][^0])self.lbl_quality.grid()self.combo_quality.grid()else:self.lbl_quality.grid_remove()self.combo_quality.grid_remove()# ----------------------------------------------------------# 辅助# ----------------------------------------------------------def_toggle_key_visibility(self):ifself.entry_api_key["show"] == "●":self.entry_api_key.config(show="")self.btn_show_key.config(text="🔒")else:self.entry_api_key.config(show="●")self.btn_show_key.config(text="👁")def_set_status(self, text):self.status_var.set(text)self.root.update_idletasks()def_update_nav(self): n = len(self.generated_images)if n == 0:self.lbl_nav.config(text="")self.btn_prev.config(state=tk.DISABLED)self.btn_next.config(state=tk.DISABLED)self.btn_save.config(state=tk.DISABLED)self.btn_save_all.config(state=tk.DISABLED)elif n == 1:self.lbl_nav.config(text="1 / 1")self.btn_prev.config(state=tk.DISABLED)self.btn_next.config(state=tk.DISABLED)self.btn_save.config(state=tk.NORMAL)self.btn_save_all.config(state=tk.DISABLED)else: idx = self.current_image_index + 1self.lbl_nav.config(text=f"{idx} / {n}")self.btn_prev.config(state=tk.NORMAL ifself.current_image_index > 0else tk.DISABLED)self.btn_next.config(state=tk.NORMAL ifself.current_image_index < n - 1else tk.DISABLED)self.btn_save.config(state=tk.NORMAL)self.btn_save_all.config(state=tk.NORMAL)def_display_image(self, img: Image.Image):"""在 Canvas 上居中显示图片"""self.canvas.delete("all") cw = self.canvas.winfo_width() ch = self.canvas.winfo_height()if cw < 10or ch < 10: cw, ch = 800, 300# 等比缩放 img_w, img_h = img.size scale = min(cw / img_w, ch / img_h, 1.0) new_w = int(img_w * scale) new_h = int(img_h * scale) resized = img.resize((new_w, new_h), Image.LANCZOS)self._tk_photo = ImageTk.PhotoImage(resized) x = (cw - new_w) // 2 y = (ch - new_h) // 2self.canvas.create_image(x, y, anchor=tk.NW, image=self._tk_photo)# ----------------------------------------------------------# 生成逻辑# ----------------------------------------------------------def_on_generate(self):ifself.is_generating:return# 校验 api_base = self.api_base.get().strip() api_key = self.api_key.get().strip() model = self.model_name.get().strip() prompt = self.txt_prompt.get("1.0", tk.END).strip()ifnot api_base: messagebox.showwarning("提示", "请填写 API 网址")returnifnot api_key: messagebox.showwarning("提示", "请填写 API Key")returnifnot model: messagebox.showwarning("提示", "请填写模型名称")returnifnot prompt: messagebox.showwarning("提示", "请输入绘图描述")return# 禁用按钮self.is_generating = Trueself.btn_generate.config(state=tk.DISABLED, text="⏳ 生成中...")self._set_status("正在生成图片,请稍候...")# 获取预设配置 preset_name = self.combo_preset.get() preset_cfg = PRESET_MODELS.get(preset_name, {})# 在后台线程中执行请求 thread = threading.Thread( target=self._generate_thread, args=(api_base, api_key, model, prompt, preset_cfg), daemon=True, ) thread.start()def_generate_thread(self, api_base, api_key, model, prompt, preset_cfg):"""后台线程:调用 API 并在画布上显示结果"""try: images = self._call_api(api_base, api_key, model, prompt, preset_cfg)if images:self.generated_images = imagesself.current_image_index = 0self.root.after(0, self._show_result, images)else:self.root.after(0, self._show_error, "未生成任何图片,请检查参数或 API 响应。")except Exception as e:self.root.after(0, self._show_error, str(e))finally:self.is_generating = Falseself.root.after(0, self._reset_generate_button)def_call_api(self, api_base, api_key, model, prompt, preset_cfg) -> list:"""调用 OpenAI 兼容的图像生成 API""" url = api_base.rstrip("/") + "/images/generations" headers = {"Content-Type": "application/json","Authorization": f"Bearer {api_key}", } body: dict = {"model": model,"prompt": prompt,"n": self.n_var.get(),"size": self.image_size.get().replace("*", "x"), }# 可选参数if preset_cfg.get("style_options"): body["style"] = self.image_style.get()if preset_cfg.get("quality_options"): body["quality"] = self.image_quality.get()# response_format 优先 base64,避免 URL 过期问题 body["response_format"] = "b64_json" resp = requests.post(url, headers=headers, json=body, timeout=120) resp.raise_for_status() data = resp.json()# 解析响应 images: list[Image.Image] = []for item in data.get("data", []):if"b64_json"in item: img_data = base64.b64decode(item["b64_json"]) img = Image.open(BytesIO(img_data)).convert("RGBA") images.append(img)elif"url"in item: img_resp = requests.get(item["url"], timeout=60) img_resp.raise_for_status() img = Image.open(BytesIO(img_resp.content)).convert("RGBA") images.append(img)elif"image"in item:# 某些 API 直接返回 base64 字段 img_data = base64.b64decode(item["image"]) img = Image.open(BytesIO(img_data)).convert("RGBA") images.append(img)return imagesdef_show_result(self, images):self._display_image(images[^0])self._update_nav()self._set_status(f"✅ 成功生成 {len(images)} 张图片")def_show_error(self, msg): messagebox.showerror("生成失败", msg)self._set_status(f"❌ 生成失败:{msg}")def_reset_generate_button(self):self.btn_generate.config(state=tk.NORMAL, text="✨ 开始生成")# ----------------------------------------------------------# 图片导航# ----------------------------------------------------------def_on_prev_image(self):ifself.current_image_index > 0:self.current_image_index -= 1self._display_image(self.generated_images[self.current_image_index])self._update_nav()def_on_next_image(self):ifself.current_image_index < len(self.generated_images) - 1:self.current_image_index += 1self._display_image(self.generated_images[self.current_image_index])self._update_nav()# ----------------------------------------------------------# 保存# ----------------------------------------------------------def_on_save(self):ifnotself.generated_images:return img = self.generated_images[self.current_image_index]self._save_single_image(img, self.current_image_index)def_on_save_all(self):ifnotself.generated_images:return# 弹出选择目录 folder = filedialog.askdirectory(title="选择保存目录", initialdir=str(self.output_dir))ifnot folder:return saved = []for i, img inenumerate(self.generated_images): ts = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"ai_image_{ts}_{i + 1}.png" filepath = Path(folder) / filename img.save(str(filepath), "PNG") saved.append(str(filepath))self._set_status(f"✅ 已保存 {len(saved)} 张图片到 {folder}") messagebox.showinfo("保存成功", f"已保存 {len(saved)} 张图片到:\n{folder}")def_save_single_image(self, img, index): filepath = filedialog.asksaveasfilename( title="保存图片", initialdir=str(self.output_dir), initialfile=f"ai_image_{index + 1}.png", defaultextension=".png", filetypes=[ ("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("WebP 图片", "*.webp"), ("所有文件", "*.*"), ], )ifnot filepath:return fmt = Path(filepath).suffix.lstrip(".").upper()if fmt == "JPG": fmt = "JPEG" img = img.convert("RGB") img.save(filepath, fmt)self._set_status(f"✅ 已保存: {filepath}")# ----------------------------------------------------------# 关闭# ----------------------------------------------------------def_on_close(self):# 保存配置(不含 API Key) config = {"api_base": self.api_base.get().strip(),"model_name": self.model_name.get().strip(),"api_key": self.api_key.get().strip(),"last_preset": self.combo_preset.get(), } save_config(config)self.root.destroy()# ============================================================# 入口# ============================================================defmain(): root = tk.Tk()# Windows DPI 适配try:from ctypes import windll windll.shcore.SetProcessDpiAwareness(1)except Exception:pass app = AIImageGenerator(root) root.mainloop()if __name__ == "__main__": main() 分析该程序中存在的问题再通过cherrystudio,利用glm-5.1进行代码审查,虽然先前代码也是Hermes agent通过glm-5.1进行撰写。但是审查中也有很多问题。 这份 Python 代码整体结构清晰,功能完整,使用了多线程避免 GUI 卡顿,并且考虑了 API 兼容性和图片防垃圾回收(self._tk_photo)等细节,是一份不错的代码。经过仔细审查,程序中仍存在一些安全隐患、逻辑漏洞、用户体验缺陷以及冗余代码。以下是详细的问题分析和修改建议:
一、 安全隐患(严重)
1. API Key 明文保存到本地文件 在 _on_close 方法中,程序将 api_key 直接写入了 ~/.ai_image_generator_config.json。这会导致你的 API Key 以明文形式存储在硬盘中,任何能访问该文件的软件或用户都能窃取你的密钥,造成财产损失。
修改建议: 方案 A:不保存 API Key,每次启动手动输入(最安全)。 方案 B:使用系统的 Keyring(钥匙串)保存,需要引入 keyring库。方案 C:如果一定要用 JSON 保存,至少进行简单的混淆或加密,并在 UI 上增加“记住 Key”的勾选框,让用户自己承担风险。
二、 逻辑漏洞与 Bug(中等)
1. ScrolledText 不支持 textvariable 在 _build_ui 中,你定义了 self.prompt_text = tk.StringVar(),并在 ScrolledText 中尝试使用(虽然代码里你其实没写 textvariable=self.prompt_text,但声明了这个变量)。而在 _on_generate 中,你正确使用了 self.txt_prompt.get("1.0", tk.END)。这说明 self.prompt_text 是死代码,Tkinter 的 Text 控件不支持 textvariable,只有 Entry 支持。
修改建议:删除 self.prompt_text = tk.StringVar()。
2. DALL-E 3 不支持生成数量 n > 1 在参数设置中,self.spin_n 允许设置 1-4。但是 OpenAI 的 DALL-E 3 模型严格限制 n 只能为 1。如果用户选择 DALL-E 3 并设置 n=2,API 会报错。
修改建议:在 PRESET_MODELS中增加max_n字段,在_on_preset_changed中动态更新Spinbox的上限。
3. API 报错信息不清晰 在 _call_api 中使用了 resp.raise_for_status()。如果 API 返回 400/401/429 等错误,这行代码只会抛出一个笼统的 requests.exceptions.HTTPError,丢失了 API 返回的具体错误原因(如“余额不足”、“内容违规”),用户看到的只是一头雾水的报错。
4. 窗口关闭时的多线程竞态条件 如果用户在图片生成过程中(后台线程正在运行)直接关闭窗口,后台线程执行完 self._call_api 后,会调用 self.root.after(0, self._show_result, images)。但此时 root 已经被销毁,会导致 TclError 崩溃。
三、 用户体验缺陷(轻微)
1. 窗口缩放时,图片预览不会自适应_display_image 方法在初次显示图片时能正常缩放,但当你拖动窗口边缘改变大小时,Canvas 中的图片不会自动重新缩放,导致图片变形或留白。
2. 误导性的依赖安装注释 在文件开头的注释中写了 pip install tkinter。这是错误的,tkinter 是 Python 的标准库,通常随 Python 一起安装,无法通过 pip 安装(Linux 下需要 sudo apt install python3-tk)。这会误导新手。
3. 通义万相的尺寸分隔符问题 通义万相的 sizes 定义为 ["1024*1024", ...],而在 _call_api 中你用 self.image_size.get().replace("*", "x") 统一转成了 x。虽然通义万相的 OpenAI 兼容接口可能现在支持 x,但 UI 上显示 1024*1024 而 API 发送 1024x1024 容易让调试的用户产生困惑。建议在配置中统一使用 x。
4. 生成按钮防抖逻辑不严谨 在 _on_generate 中,如果 self.is_generating 为 True,直接 return 了,用户没有任何提示。如果网络很慢,用户可能会疯狂点击。
修改建议:生成中时,给用户一个状态反馈,或者让按钮变成不可用状态(其实你在后台线程里做了 self.btn_generate.config(state=tk.DISABLED),但在主线程校验前没做,存在快速双击的可能)。
四、 代码冗余与规范
1. 重复导入 requests 代码第 11 行和第 20 行两次尝试 import requests。
修改建议:删除第 11 行的 import requests,只保留下方的带try...except的导入。
2. PRESET_MODELS 中的 endpoint 字段未被使用 配置中写了 "endpoint": "/images/generations",但在 _call_api 中你是硬编码拼接的:url = api_base.rstrip("/") + "/images/generations"。
最后测试过程中发现存在的问题如下:
1、原始的对话框小了,部分按钮无法完全显示;2、高分屏的自适应问题; **3、输入网址和api时,增加刷新按钮,实现生图模型的列表,在列表中选择模型用于本次生图。
针对你的需求,我进行了全面升级。以下是主要修改说明:
API Key 自动保存:恢复了 API Key 的本地保存功能,下次启动程序时会自动填入上次的 Key。 新增高级参数区:增加了“高级参数”面板,包含你要求的 5 项控制:
随机种子数 (Seed):留空则随机,填入数字可复现图像。 推理步数 (Steps):1-50,默认 20。 引导比例 (CFG):0-20,默认 7.5。 反向提示词 (Negative Prompt):描述不想出现的内容。 提示词增强 (Prompt Enhancement):勾选后开启。
智能参数过滤:由于 OpenAI DALL-E 系列不支持步数、引导比例、反向提示词等 SD/Flux 特有参数,如果强行发送会报错。代码中增加了智能判断:当选择 DALL-E 时,自动屏蔽这些参数的发送,确保 API 调用不会报错;当选择硅基流动等模型时,则完整发送。
最后存在一个关键性问题,选定某个AI供应商后,输入api,其他模型也随之采用该api,合理的应该是每个模型供应商,api单独存放一个位置,其他模型供应商的仍为空。 随后对代码进行了重构,实现了 API 配置按供应商独立隔离存储:
独立存储:每个预设模型(如硅基流动、智谱、DALL-E)现在都有自己独立的 api_base和api_key存储空间。自动切换:当你选择“硅基流动”时填入其 Key,切换到“智谱”时界面会自动清空并显示智谱的配置;再切回“硅基流动”时,之前填的 Key 会自动恢复。 自动保存:在切换预设或关闭程序时,当前供应商的配置会自动保存到本地,下次打开无需重填。
最终的完整代码:
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""AI 图像生成工具 + 模型列表获取=====================================功能:通过 GUI 对话框界面,调用支持 OpenAI 兼容 API 的图像生成模型进行绘图。支持平台:OpenAI DALL-E、Stability AI、硅基流动、通义万相、智谱 CogView 等。依赖安装: pip install requests pip install Pillow使用方法: python ai_image_generator.py"""import tkinter as tkfrom tkinter import ttk, messagebox, filedialog, scrolledtextimport requestsimport jsonimport base64import osimport sysimport threadingfrom datetime import datetimefrom pathlib import Pathfrom io import BytesIOtry:from PIL import Image, ImageTkexcept ImportError:print("❌ 缺少 Pillow 库,请执行:pip install Pillow") sys.exit(1)try:import requestsexcept ImportError:print("❌ 缺少 requests 库,请执行:pip install requests") sys.exit(1)# ============================================================# 资源路径兼容函数 (支持 PyInstaller 打包)# ============================================================defresource_path(relative_path):"""获取资源绝对路径(兼容开发环境和 PyInstaller 打包后)"""ifhasattr(sys, '_MEIPASS'): base_path = sys._MEIPASSelse: base_path = os.path.abspath(".")return os.path.join(base_path, relative_path)# ============================================================# 预设模型配置# ============================================================PRESET_MODELS = {"OpenAI DALL-E 3": {"api_base": "https://api.openai.com/v1","model": "dall-e-3","sizes": ["1024x1024", "1024x1792", "1792x1024"],"default_size": "1024x1024","style_options": ["vivid", "natural"],"quality_options": ["standard", "hd"],"note": "需要 OpenAI API Key,支持 DALL-E 3 (仅支持单张生成)", },"OpenAI DALL-E 2": {"api_base": "https://api.openai.com/v1","model": "dall-e-2","sizes": ["256x256", "512x512", "1024x1024"],"default_size": "1024x1024","style_options": None,"quality_options": None,"note": "需要 OpenAI API Key", },"硅基流动 SiliconFlow": {"api_base": "https://api.siliconflow.cn/v1","model": "stabilityai/stable-diffusion-3-medium","sizes": ["1024x1024", "1024x768", "768x1024", "512x512"],"default_size": "1024x1024","style_options": None,"quality_options": None,"note": "国内可用,支持多种开源模型,完美支持高级参数", },"智谱 CogView-3": {"api_base": "https://open.bigmodel.cn/api/paas/v4","model": "cogview-3-plus","sizes": ["1024x1024", "768x1344", "864x1152", "1344x768", "1152x864", "1440x720", "720x1440"],"default_size": "1024x1024","style_options": None,"quality_options": None,"note": "智谱清言出品,国内可用", },"通义万相": {"api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1","model": "wanx-v1","sizes": ["1024x1024", "720x1280", "1280x720"],"default_size": "1024x1024","style_options": None,"quality_options": None,"note": "阿里云通义万相,国内可用", },"自定义 OpenAI 兼容": {"api_base": "","model": "","sizes": ["1024x1024", "512x512", "256x256"],"default_size": "1024x1024","style_options": None,"quality_options": None,"note": "适用于任何 OpenAI 兼容的图像生成 API", },}# ============================================================# 配置持久化# ============================================================CONFIG_FILE = Path.home() / ".ai_image_generator_config.json"defload_config():if CONFIG_FILE.exists():try:withopen(CONFIG_FILE, "r", encoding="utf-8") as f:return json.load(f)except Exception:passreturn {}defsave_config(config):try:withopen(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2)except Exception:pass# ============================================================# 主应用程序# ============================================================classAIImageGenerator:def__init__(self, root: tk.Tk):self.root = rootself.root.title("🎨 AI 图像生成工具")self.root.geometry("1100x1200")self.root.minsize(900, 900)# 状态变量self.generated_images = []self.current_image_index = 0self.is_generating = Falseself.is_fetching_models = Falseself.api_key = tk.StringVar()self.api_base = tk.StringVar()self.model_name = tk.StringVar()self.image_size = tk.StringVar(value="1024x1024")self.image_style = tk.StringVar(value="vivid")self.image_quality = tk.StringVar(value="standard")self.n_var = tk.IntVar(value=1)# 高级参数变量self.seed_var = tk.StringVar(value="") # 留空表示随机self.steps_var = tk.IntVar(value=20)self.guidance_var = tk.DoubleVar(value=7.5)self.negative_prompt_var = tk.StringVar(value="")self.enhance_var = tk.BooleanVar(value=False)# 加载配置 (新逻辑:按供应商独立加载)self.config_data = load_config()self.presets_config = self.config_data.get("presets_config", {})self._current_preset_name = None# 记录当前所处的预设名称self.output_dir = Path.home() / "AI_Generated_Images"self.output_dir.mkdir(exist_ok=True)self._build_styles()self._build_ui()# 触发初始预设加载 last_preset = self.config_data.get("last_preset", "OpenAI DALL-E 3")if last_preset in PRESET_MODELS:self.combo_preset.set(last_preset)else:self.combo_preset.current(0)self._on_preset_changed() # 手动触发第一次加载def_build_styles(self): style = ttk.Style()try: style.theme_use("clam")except Exception:pass style.configure("Title.TLabel", font=("Microsoft YaHei UI", 14, "bold")) style.configure("Section.TLabel", font=("Microsoft YaHei UI", 10, "bold")) style.configure("TButton", font=("Microsoft YaHei UI", 10), padding=6) style.configure("Generate.TButton", font=("Microsoft YaHei UI", 11, "bold"), padding=8) style.configure("TLabelframe.Label", font=("Microsoft YaHei UI", 10, "bold")) style.configure("Status.TLabel", font=("Microsoft YaHei UI", 9), foreground="gray")def_build_ui(self): outer = ttk.Frame(self.root, padding=10) outer.pack(fill=tk.BOTH, expand=True) ttk.Label(outer, text="🎨 AI 图像生成工具", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 10))# ============ 上半部分:设置区 ============ top_frame = ttk.Frame(outer) top_frame.pack(fill=tk.X, pady=(0, 6))# --- 预设选择 --- preset_frame = ttk.LabelFrame(top_frame, text="预设模型", padding=8) preset_frame.pack(fill=tk.X, pady=(0, 6))self.combo_preset = ttk.Combobox(preset_frame, values=list(PRESET_MODELS.keys()), state="readonly", width=35, font=("Microsoft YaHei UI", 10))self.combo_preset.pack(side=tk.LEFT, padx=(0, 10))self.combo_preset.bind("<<ComboboxSelected>>", self._on_preset_changed)self.lbl_note = ttk.Label(preset_frame, text="", style="Status.TLabel", wraplength=500)self.lbl_note.pack(side=tk.LEFT, fill=tk.X, expand=True)# --- API 设置 --- api_frame = ttk.LabelFrame(top_frame, text="API 设置", padding=8) api_frame.pack(fill=tk.X, pady=(0, 6)) ttk.Label(api_frame, text="API 网址:").grid(row=0, column=0, sticky=tk.W, pady=4)self.entry_api_base = ttk.Entry(api_frame, textvariable=self.api_base, width=65, font=("Consolas", 10))self.entry_api_base.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=4) ttk.Label(api_frame, text="API Key:").grid(row=1, column=0, sticky=tk.W, pady=4)self.entry_api_key = ttk.Entry(api_frame, textvariable=self.api_key, width=65, show="●", font=("Consolas", 10))self.entry_api_key.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=4)self.btn_show_key = ttk.Button(api_frame, text="👁", width=3, command=self._toggle_key_visibility)self.btn_show_key.grid(row=1, column=2, padx=2, pady=4) ttk.Label(api_frame, text="模型名称:").grid(row=2, column=0, sticky=tk.W, pady=4) model_input_frame = ttk.Frame(api_frame) model_input_frame.grid(row=2, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=4)self.combo_model = ttk.Combobox(model_input_frame, textvariable=self.model_name, width=50, font=("Consolas", 10))self.combo_model.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))self.btn_refresh_models = ttk.Button(model_input_frame, text="🔄 刷新模型", command=self._on_refresh_models)self.btn_refresh_models.pack(side=tk.RIGHT) api_frame.columnconfigure(1, weight=1)# --- 基础参数设置 --- param_frame = ttk.LabelFrame(top_frame, text="基础参数", padding=8) param_frame.pack(fill=tk.X, pady=(0, 6)) ttk.Label(param_frame, text="图片尺寸:").grid(row=0, column=0, sticky=tk.W, pady=2)self.combo_size = ttk.Combobox(param_frame, textvariable=self.image_size, state="readonly", width=15, font=("Consolas", 10))self.combo_size.grid(row=0, column=1, padx=5, pady=2) ttk.Label(param_frame, text="生成数量:").grid(row=0, column=2, sticky=tk.W, padx=(20, 0), pady=2)self.spin_n = ttk.Spinbox(param_frame, from_=1, to=4, textvariable=self.n_var, width=5, font=("Consolas", 10))self.spin_n.grid(row=0, column=3, padx=5, pady=2)self.lbl_style = ttk.Label(param_frame, text="风格:")self.lbl_style.grid(row=1, column=0, sticky=tk.W, pady=2)self.combo_style = ttk.Combobox(param_frame, textvariable=self.image_style, state="readonly", width=15, font=("Consolas", 10))self.combo_style.grid(row=1, column=1, padx=5, pady=2)self.lbl_quality = ttk.Label(param_frame, text="质量:")self.lbl_quality.grid(row=1, column=2, sticky=tk.W, padx=(20, 0), pady=2)self.combo_quality = ttk.Combobox(param_frame, textvariable=self.image_quality, state="readonly", width=15, font=("Consolas", 10))self.combo_quality.grid(row=1, column=3, padx=5, pady=2)# --- 高级参数设置 (新增) --- adv_frame = ttk.LabelFrame(top_frame, text="高级参数 (适用于SD/Flux类模型,DALL-E将自动忽略)", padding=8) adv_frame.pack(fill=tk.X, pady=(0, 6))# 第一行:种子、步数、引导比例 ttk.Label(adv_frame, text="随机种子:").grid(row=0, column=0, sticky=tk.W, pady=2)self.entry_seed = ttk.Entry(adv_frame, textvariable=self.seed_var, width=15, font=("Consolas", 10))self.entry_seed.grid(row=0, column=1, padx=5, pady=2) ttk.Label(adv_frame, text="(留空随机)", style="Status.TLabel").grid(row=0, column=2, sticky=tk.W, pady=2) ttk.Label(adv_frame, text="推理步数:").grid(row=0, column=3, sticky=tk.W, padx=(20, 0), pady=2)self.spin_steps = ttk.Spinbox(adv_frame, from_=1, to=50, textvariable=self.steps_var, width=5, font=("Consolas", 10))self.spin_steps.grid(row=0, column=4, padx=5, pady=2) ttk.Label(adv_frame, text="引导比例:").grid(row=0, column=5, sticky=tk.W, padx=(20, 0), pady=2)self.spin_guidance = ttk.Spinbox(adv_frame, from_=0, to=20, increment=0.5, textvariable=self.guidance_var, width=5, font=("Consolas", 10))self.spin_guidance.grid(row=0, column=6, padx=5, pady=2)# 第二行:反向提示词 ttk.Label(adv_frame, text="反向提示词:").grid(row=1, column=0, sticky=tk.W, pady=2)self.entry_neg_prompt = ttk.Entry(adv_frame, textvariable=self.negative_prompt_var, width=60, font=("Microsoft YaHei UI", 10))self.entry_neg_prompt.grid(row=1, column=1, columnspan=5, sticky=tk.EW, padx=5, pady=2)# 第三行:提示词增强开关self.chk_enhance = ttk.Checkbutton(adv_frame, text="开启提示词增强 (自动重写为详细提示词)", variable=self.enhance_var)self.chk_enhance.grid(row=2, column=0, columnspan=7, sticky=tk.W, pady=2) adv_frame.columnconfigure(1, weight=1) adv_frame.columnconfigure(5, weight=1)# --- Prompt --- prompt_frame = ttk.LabelFrame(top_frame, text="绘图描述", padding=8) prompt_frame.pack(fill=tk.X, pady=(0, 6))self.txt_prompt = scrolledtext.ScrolledText(prompt_frame, height=3, font=("Microsoft YaHei UI", 10), wrap=tk.WORD)self.txt_prompt.pack(fill=tk.X, expand=True)# ============ 固定在中间的操作按钮区 ============ btn_frame = ttk.Frame(outer) btn_frame.pack(fill=tk.X, pady=8)self.btn_generate = ttk.Button(btn_frame, text="✨ 开始生成", style="Generate.TButton", command=self._on_generate)self.btn_generate.pack(side=tk.LEFT, padx=(0, 10))self.btn_save = ttk.Button(btn_frame, text="💾 保存图片", command=self._on_save, state=tk.DISABLED)self.btn_save.pack(side=tk.LEFT, padx=(0, 10))self.btn_save_all = ttk.Button(btn_frame, text="📁 保存全部", command=self._on_save_all, state=tk.DISABLED)self.btn_save_all.pack(side=tk.LEFT, padx=(0, 10))self.btn_prev = ttk.Button(btn_frame, text="◀ 上一张", width=8, command=self._on_prev_image, state=tk.DISABLED)self.btn_prev.pack(side=tk.RIGHT, padx=2)self.btn_next = ttk.Button(btn_frame, text="下一张 ▶", width=8, command=self._on_next_image, state=tk.DISABLED)self.btn_next.pack(side=tk.RIGHT, padx=2)self.lbl_nav = ttk.Label(btn_frame, text="", style="Status.TLabel")self.lbl_nav.pack(side=tk.RIGHT, padx=10)# ============ 下半部分:图片预览 ============ preview_frame = ttk.LabelFrame(outer, text="图片预览", padding=5) preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 4))self.canvas = tk.Canvas(preview_frame, bg="#f0f0f0", highlightthickness=0)self.canvas.pack(fill=tk.BOTH, expand=True)self.canvas.bind("<Configure>", self._on_canvas_resize)# 状态栏self.status_var = tk.StringVar(value="就绪 — 选择预设模型或填写 API 信息后开始生成")self.lbl_status = ttk.Label(outer, textvariable=self.status_var, style="Status.TLabel", anchor=tk.W)self.lbl_status.pack(fill=tk.X)self.root.protocol("WM_DELETE_WINDOW", self._on_close)self.root.bind("<Control-s>", lambda e: self._on_save())self.root.bind("<Control-Return>", lambda e: self._on_generate())# ----------------------------------------------------------# 预设切换与独立配置保存逻辑# ----------------------------------------------------------def_save_current_preset_config(self, preset_name):"""将当前界面上的 API 配置保存到指定的预设名下"""ifnot preset_name or preset_name notin PRESET_MODELS:returnif preset_name notinself.presets_config:self.presets_config[preset_name] = {}# 保存当前输入框的值self.presets_config[preset_name]["api_base"] = self.api_base.get().strip()self.presets_config[preset_name]["api_key"] = self.api_key.get().strip()def_on_preset_changed(self, event=None): new_name = self.combo_preset.get()# 1. 保存之前预设的 API 信息ifself._current_preset_name:self._save_current_preset_config(self._current_preset_name)# 2. 更新当前预设名称记录self._current_preset_name = new_nameif new_name notin PRESET_MODELS:return cfg = PRESET_MODELS[new_name]# 3. 加载新预设的 API 信息 (如果之前没保存过,则使用预设默认值,Key为空) saved_preset_cfg = self.presets_config.get(new_name, {})self.api_base.set(saved_preset_cfg.get("api_base", cfg.get("api_base", "")))self.api_key.set(saved_preset_cfg.get("api_key", ""))# 4. 其他原有逻辑 (重置模型、尺寸等)self.model_name.set(cfg["model"])self.lbl_note.config(text=cfg.get("note", ""))self.combo_size["values"] = cfg["sizes"]self.image_size.set(cfg["default_size"])if cfg.get("style_options"):self.combo_style["values"] = cfg["style_options"]self.image_style.set(cfg["style_options"][0])self.lbl_style.grid()self.combo_style.grid()else:self.lbl_style.grid_remove()self.combo_style.grid_remove()if cfg.get("quality_options"):self.combo_quality["values"] = cfg["quality_options"]self.image_quality.set(cfg["quality_options"][0])self.lbl_quality.grid()self.combo_quality.grid()else:self.lbl_quality.grid_remove()self.combo_quality.grid_remove()if"DALL-E 3"in new_name:self.spin_n.config(to=1)self.n_var.set(1)else:self.spin_n.config(to=4)# ----------------------------------------------------------# 模型列表刷新逻辑# ----------------------------------------------------------def_on_refresh_models(self):ifself.is_fetching_models:return api_base = self.api_base.get().strip() api_key = self.api_key.get().strip()ifnot api_base: messagebox.showwarning("提示", "请先填写 API 网址")returnifnot api_key: messagebox.showwarning("提示", "请先填写 API Key")returnself.is_fetching_models = Trueself.btn_refresh_models.config(state=tk.DISABLED, text="⏳ 获取中...")self._set_status("正在从 API 获取模型列表...") thread = threading.Thread( target=self._fetch_models_thread, args=(api_base, api_key), daemon=True ) thread.start()def_fetch_models_thread(self, api_base, api_key):try: url = api_base.rstrip("/") + "/models" headers = {"Authorization": f"Bearer {api_key}"} resp = requests.get(url, headers=headers, timeout=15)if resp.status_code != 200:try: err_data = resp.json() msg = err_data.get("error", {}).get("message", str(err_data))except: msg = resp.textraise Exception(f"HTTP {resp.status_code} - {msg}") data = resp.json() all_models = [m.get("id") for m in data.get("data", []) if m.get("id")]ifnot all_models:raise Exception("API 返回的模型列表为空") image_keywords = ["dall", "stable-diffusion", "sd", "cogview", "wanx", "flux", "midjourney", "paint", "image", "visual"] filtered_models = [m for m in all_models ifany(kw in m.lower() for kw in image_keywords)] final_models = filtered_models if filtered_models else all_modelsifself.root.winfo_exists():self.root.after(0, self._update_model_list, final_models)except Exception as e:ifself.root.winfo_exists():self.root.after(0, self._show_error, f"获取模型列表失败: {e}")finally:self.is_fetching_models = Falseifself.root.winfo_exists():self.root.after(0, lambda: self.btn_refresh_models.config(state=tk.NORMAL, text="🔄 刷新模型"))def_update_model_list(self, models): current_model = self.model_name.get()self.combo_model["values"] = sorted(models)if current_model in models:self.combo_model.set(current_model)elif models:self.combo_model.current(0)self._set_status(f"✅ 成功获取 {len(models)} 个模型(可在下拉列表中选择或手动输入)")# ----------------------------------------------------------# 辅助# ----------------------------------------------------------def_toggle_key_visibility(self):ifself.entry_api_key["show"] == "●":self.entry_api_key.config(show="")self.btn_show_key.config(text="🔒")else:self.entry_api_key.config(show="●")self.btn_show_key.config(text="👁")def_set_status(self, text):self.status_var.set(text)self.root.update_idletasks()def_update_nav(self): n = len(self.generated_images)if n == 0:self.lbl_nav.config(text="")self.btn_prev.config(state=tk.DISABLED)self.btn_next.config(state=tk.DISABLED)self.btn_save.config(state=tk.DISABLED)self.btn_save_all.config(state=tk.DISABLED)elif n == 1:self.lbl_nav.config(text="1 / 1")self.btn_prev.config(state=tk.DISABLED)self.btn_next.config(state=tk.DISABLED)self.btn_save.config(state=tk.NORMAL)self.btn_save_all.config(state=tk.DISABLED)else: idx = self.current_image_index + 1self.lbl_nav.config(text=f"{idx} / {n}")self.btn_prev.config(state=tk.NORMAL ifself.current_image_index > 0else tk.DISABLED)self.btn_next.config(state=tk.NORMAL ifself.current_image_index < n - 1else tk.DISABLED)self.btn_save.config(state=tk.NORMAL)self.btn_save_all.config(state=tk.NORMAL)def_display_image(self, img: Image.Image):self.canvas.delete("all") cw = self.canvas.winfo_width() ch = self.canvas.winfo_height()if cw < 10or ch < 10: cw, ch = 800, 400 img_w, img_h = img.size scale = min(cw / img_w, ch / img_h, 1.0) new_w = max(1, int(img_w * scale)) new_h = max(1, int(img_h * scale)) resized = img.resize((new_w, new_h), Image.LANCZOS)self._tk_photo = ImageTk.PhotoImage(resized) x = (cw - new_w) // 2 y = (ch - new_h) // 2self.canvas.create_image(x, y, anchor=tk.NW, image=self._tk_photo)def_on_canvas_resize(self, event=None):ifself.generated_images:self._display_image(self.generated_images[self.current_image_index])# ----------------------------------------------------------# 生成逻辑# ----------------------------------------------------------def_on_generate(self):ifself.is_generating:return api_base = self.api_base.get().strip() api_key = self.api_key.get().strip() model = self.model_name.get().strip() prompt = self.txt_prompt.get("1.0", tk.END).strip()ifnot api_base: messagebox.showwarning("提示", "请填写 API 网址")returnifnot api_key: messagebox.showwarning("提示", "请填写 API Key")returnifnot model: messagebox.showwarning("提示", "请填写或选择模型名称")returnifnot prompt: messagebox.showwarning("提示", "请输入绘图描述")returnself.is_generating = Trueself.btn_generate.config(state=tk.DISABLED, text="⏳ 生成中...")self._set_status("正在生成图片,请稍候...") preset_name = self.combo_preset.get() preset_cfg = PRESET_MODELS.get(preset_name, {}) thread = threading.Thread( target=self._generate_thread, args=(api_base, api_key, model, prompt, preset_cfg, preset_name), daemon=True, ) thread.start()def_generate_thread(self, api_base, api_key, model, prompt, preset_cfg, preset_name):try: images = self._call_api(api_base, api_key, model, prompt, preset_cfg, preset_name)if images:self.generated_images = imagesself.current_image_index = 0ifself.root.winfo_exists():self.root.after(0, self._show_result, images)else:ifself.root.winfo_exists():self.root.after(0, self._show_error, "未生成任何图片,请检查参数或 API 响应。")except Exception as e:ifself.root.winfo_exists():self.root.after(0, self._show_error, str(e))finally:self.is_generating = Falseifself.root.winfo_exists():self.root.after(0, self._reset_generate_button)def_call_api(self, api_base, api_key, model, prompt, preset_cfg, preset_name) -> list: url = api_base.rstrip("/") + "/images/generations" headers = {"Content-Type": "application/json","Authorization": f"Bearer {api_key}", } body = {"model": model,"prompt": prompt,"n": self.n_var.get(),"size": self.image_size.get().replace("*", "x"), }# 基础参数 (DALL-E 专属)if preset_cfg.get("style_options"): body["style"] = self.image_style.get()if preset_cfg.get("quality_options"): body["quality"] = self.image_quality.get() body["response_format"] = "b64_json"# 高级参数 (仅对非 DALL-E 模型生效,防止 API 报无效参数错误) is_dalle = "DALL-E"in preset_nameifnot is_dalle:# 种子数 seed_str = self.seed_var.get().strip()if seed_str:try: body["seed"] = int(seed_str)except ValueError:pass# 忽略非数字输入# 推理步数 steps = self.steps_var.get()if steps > 0: body["num_inference_steps"] = steps# 引导比例 guidance = self.guidance_var.get()if guidance > 0: body["guidance_scale"] = guidance# 反向提示词 neg_prompt = self.negative_prompt_var.get().strip()if neg_prompt: body["negative_prompt"] = neg_prompt# 提示词增强ifself.enhance_var.get(): body["prompt_enhancement"] = Trueelse:# DALL-E 3 近期也支持了 seed 参数,可以加上 seed_str = self.seed_var.get().strip()if seed_str:try: body["seed"] = int(seed_str)except ValueError:pass resp = requests.post(url, headers=headers, json=body, timeout=120)if resp.status_code != 200:try: err_data = resp.json() msg = err_data.get("error", {}).get("message", str(err_data))except: msg = resp.textraise Exception(f"API 错误 ({resp.status_code}): {msg}") data = resp.json() images = []for item in data.get("data", []):if"b64_json"in item: img_data = base64.b64decode(item["b64_json"]) img = Image.open(BytesIO(img_data))# 修复 libpng icc profile 警告if"icc_profile"in img.info:del img.info["icc_profile"] img = img.convert("RGBA") images.append(img)elif"url"in item: img_resp = requests.get(item["url"], timeout=60) img_resp.raise_for_status() img = Image.open(BytesIO(img_resp.content))# 修复 libpng icc profile 警告if"icc_profile"in img.info:del img.info["icc_profile"] img = img.convert("RGBA") images.append(img)return imagesdef_show_result(self, images):self._display_image(images[0])self._update_nav()self._set_status(f"✅ 成功生成 {len(images)} 张图片")def_show_error(self, msg): messagebox.showerror("生成失败", msg)self._set_status(f"❌ 生成失败:{msg}")def_reset_generate_button(self):self.btn_generate.config(state=tk.NORMAL, text="✨ 开始生成")# ----------------------------------------------------------# 图片导航# ----------------------------------------------------------def_on_prev_image(self):ifself.current_image_index > 0:self.current_image_index -= 1self._display_image(self.generated_images[self.current_image_index])self._update_nav()def_on_next_image(self):ifself.current_image_index < len(self.generated_images) - 1:self.current_image_index += 1self._display_image(self.generated_images[self.current_image_index])self._update_nav()# ----------------------------------------------------------# 保存# ----------------------------------------------------------def_on_save(self):ifnotself.generated_images:return img = self.generated_images[self.current_image_index]self._save_single_image(img, self.current_image_index)def_on_save_all(self):ifnotself.generated_images:return folder = filedialog.askdirectory(title="选择保存目录", initialdir=str(self.output_dir))ifnot folder:return saved = []for i, img inenumerate(self.generated_images): ts = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"ai_image_{ts}_{i + 1}.png" filepath = Path(folder) / filename img.save(str(filepath), "PNG") saved.append(str(filepath))self._set_status(f"✅ 已保存 {len(saved)} 张图片到 {folder}") messagebox.showinfo("保存成功", f"已保存 {len(saved)} 张图片到:\n{folder}")def_save_single_image(self, img, index): filepath = filedialog.asksaveasfilename( title="保存图片", initialdir=str(self.output_dir), initialfile=f"ai_image_{index + 1}.png", defaultextension=".png", filetypes=[ ("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("WebP 图片", "*.webp"), ("所有文件", "*.*"), ], )ifnot filepath:return fmt = Path(filepath).suffix.lstrip(".").upper()if fmt == "JPG": fmt = "JPEG" img = img.convert("RGB") img.save(filepath, fmt)self._set_status(f"✅ 已保存: {filepath}")# ----------------------------------------------------------# 关闭# ----------------------------------------------------------def_on_close(self):# 1. 保存当前正在使用的预设配置ifself._current_preset_name:self._save_current_preset_config(self._current_preset_name)# 2. 将整体配置写入文件 config_to_save = {"last_preset": self._current_preset_name,"presets_config": self.presets_config } save_config(config_to_save)self.root.destroy()# ============================================================# 入口# ============================================================defmain():# Windows 高分屏 DPI 适配try:from ctypes import windll windll.shcore.SetProcessDpiAwareness(2)except Exception:try: windll.user32.SetProcessDPIAware()except Exception:pass# 1. 先创建主窗口 root = tk.Tk()# 2. 隐藏主窗口,避免在构建控件时出现从小变大的闪烁 root.withdraw()# 3. 设置图标try: ico_path = resource_path("snow.ico")if os.path.exists(ico_path): root.iconbitmap(ico_path)except Exception as e:print(f"设置图标失败: {e}")# 4. 初始化应用并构建 UI app = AIImageGenerator(root)# 5. 强制 Tkinter 计算并更新所有布局和尺寸 root.update_idletasks()# 6. 将窗口居中显示 sw = root.winfo_screenwidth() sh = root.winfo_screenheight() ww = root.winfo_reqwidth() wh = root.winfo_reqheight() x = (sw - ww) // 2 y = (sh - wh) // 2 root.geometry(f"+{x}+{y}")# 7. 一切准备就绪,显示主窗口 root.deiconify()# 8. 进入主循环 root.mainloop()if __name__ == "__main__": main()
夜雨聆风