9b6295c7 by 柴进

nanobanana初版

0 parents
1 <!-- OPENSPEC:START -->
2 # OpenSpec Instructions
3
4 These instructions are for AI assistants working in this project.
5
6 Always open `@/openspec/AGENTS.md` when the request:
7 - Mentions planning or proposals (words like proposal, spec, change, plan)
8 - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
9 - Sounds ambiguous and you need the authoritative spec before coding
10
11 Use `@/openspec/AGENTS.md` to learn:
12 - How to create and apply change proposals
13 - Spec format and conventions
14 - Project structure and guidelines
15
16 Keep this managed block so 'openspec update' can refresh the instructions.
17
18 <!-- OPENSPEC:END -->
...\ No newline at end of file ...\ No newline at end of file
1 <!-- OPENSPEC:START -->
2 # OpenSpec Instructions
3
4 These instructions are for AI assistants working in this project.
5
6 Always open `@/openspec/AGENTS.md` when the request:
7 - Mentions planning or proposals (words like proposal, spec, change, plan)
8 - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
9 - Sounds ambiguous and you need the authoritative spec before coding
10
11 Use `@/openspec/AGENTS.md` to learn:
12 - How to create and apply change proposals
13 - Spec format and conventions
14 - Project structure and guidelines
15
16 Keep this managed block so 'openspec update' can refresh the instructions.
17
18 <!-- OPENSPEC:END -->
...\ No newline at end of file ...\ No newline at end of file
1 # Gemini Image Generator
2
3 简洁的跨平台桌面应用,使用 Google Gemini API 生成图片。
4
5 ## 功能特性
6
7 - **用户认证**: 基于 MySQL 的登录系统,确保只有授权用户可访问
8 - 输入文本 Prompt 生成图片
9 - 支持上传多张参考图片
10 - 多种图片比例选择(1:1, 16:9, 4:3 等)
11 - 多种分辨率选择(1K, 2K, 4K)
12 - 支持两种模型:gemini-2.5-flash-image 和 gemini-3-pro-image-preview
13 - 实时预览生成的图片
14 - 一键下载生成的图片
15 - 跨平台支持(Windows 和 macOS)
16
17 ## 系统要求
18
19 - Python 3.8 或更高版本
20 - 有效的 Google AI API 密钥
21 - MySQL 数据库访问权限(用于用户认证)
22
23 ## 快速开始
24
25 ### 开发模式运行
26
27 1. 安装依赖:
28 ```bash
29 pip install -r requirements.txt
30 ```
31
32 2. 配置数据库和 API 密钥:
33 - 编辑 `config.json` 文件,配置以下字段:
34 ```json
35 {
36 "api_key": "你的Google AI API密钥",
37 "db_config": {
38 "host": "你的MySQL主机地址",
39 "port": 3306,
40 "user": "数据库用户名",
41 "password": "数据库密码",
42 "database": "数据库名",
43 "table": "nano_banana_users"
44 },
45 "last_user": ""
46 }
47 ```
48
49 3. 创建数据库表:
50 ```sql
51 CREATE TABLE `nano_banana_users` (
52 `user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
53 `passwd` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
54 `status` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL
55 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
56 ```
57
58 4. 添加初始用户:
59 ```bash
60 python user_util.py add admin your_password
61 ```
62
63 5. 运行应用:
64 ```bash
65 python image_generator.py
66 ```
67
68 首次运行会显示登录界面,使用创建的账户登录。
69
70 ### 打包为可执行文件
71
72 #### Windows
73
74 双击运行 `build_windows.bat` 或在命令行执行:
75 ```cmd
76 build_windows.bat
77 ```
78
79 生成的可执行文件位于 `dist\GeminiImageGenerator.exe`
80
81 #### macOS
82
83 在终端执行:
84 ```bash
85 chmod +x build_mac.sh
86 ./build_mac.sh
87 ```
88
89 生成的应用程序位于 `dist/GeminiImageGenerator.app`
90
91 ## 配置文件位置
92
93 应用会根据运行模式自动选择配置文件位置:
94
95 **开发模式**(直接运行 Python):
96 - 配置文件:`./config.json`(当前目录)
97
98 **打包后的应用**:
99 - **macOS**: `~/Library/Application Support/ZB100ImageGenerator/config.json`
100 - **Windows**: `%APPDATA%\ZB100ImageGenerator\config.json`
101 - **Linux**: `~/.config/zb100imagegenerator/config.json`
102
103 首次运行打包应用时,会自动从打包的模板复制 API 密钥到用户目录。
104
105 ## 用户管理
106
107 应用内置用户管理工具 `user_util.py`(仅管理员使用,不随客户端分发)。
108
109 ### 用户管理命令
110
111 ```bash
112 # 添加新用户
113 python user_util.py add <username> <password>
114
115 # 列出所有用户
116 python user_util.py list
117
118 # 禁用用户
119 python user_util.py disable <username>
120
121 # 启用用户
122 python user_util.py enable <username>
123
124 # 重置密码
125 python user_util.py reset <username> <new_password>
126 ```
127
128 ### 示例
129
130 ```bash
131 # 添加管理员账户
132 python user_util.py add admin MySecurePass123
133
134 # 查看所有用户
135 python user_util.py list
136
137 # 禁用某个用户
138 python user_util.py disable testuser
139
140 # 重置用户密码
141 python user_util.py reset admin NewPassword456
142 ```
143
144 ### 安全说明
145
146 - 密码使用 SHA256 哈希存储,数据库和本地均不保存明文
147 - 所有数据库操作使用参数化查询,防止 SQL 注入
148 - user_util.py 工具仅供管理员使用,不应分发给普通用户
149 - 应用会记住上次登录的用户名(可选),但不会保存密码
150
151 ## 使用说明
152
153 1. **登录应用**
154 - 应用启动时显示登录界面
155 - 输入用户名和密码
156 - 可勾选"记住用户名"选项,下次自动填充用户名
157 - 登录成功后进入主界面
158
159 2. **配置 API 密钥**
160 - 编辑配置文件中的 `api_key` 字段
161 - 或通过应用界面的收藏提示词功能自动保存
162
163 3. **上传参考图片(可选)**
164 - 点击 "添加图片" 按钮选择一张或多张参考图片
165 - 这些图片将作为生成图片的参考
166 - 可以单独删除每张图片
167
168 3. **输入 Prompt**
169 - 在提示词文本框中输入你想生成的图片描述
170 - 可以使用 "收藏" 功能保存常用提示词
171 - 从下拉菜单快速选择已保存的提示词
172
173 4. **选择生成参数**
174 - **宽高比**: 选择图片的宽高比(1:1, 16:9 等)
175 - **图片尺寸**: 选择图片的分辨率(1K/2K/4K)
176 - **AI 模型**: 选择使用的模型
177
178 5. **生成图片**
179 - 点击 "生成图片" 按钮
180 - 等待生成完成,生成的图片会显示在预览区域
181 - 双击预览图可用系统默认查看器打开
182
183 6. **下载图片**
184 - 点击 "下载图片" 按钮
185 - 选择保存位置和文件格式(PNG/JPEG)
186
187 ## 项目结构
188
189 ```
190 Nano_Banana_App/
191 ├── image_generator.py # 主程序文件(含登录界面和数据库认证)
192 ├── user_util.py # 用户管理工具(管理员专用)
193 ├── requirements.txt # Python 依赖
194 ├── config.json # 配置文件(API密钥+数据库配置)
195 ├── build_windows.bat # Windows 打包脚本
196 ├── build_mac.sh # macOS 打包脚本
197 └── README.md # 本文件
198 ```
199
200 ## 技术栈
201
202 - **GUI 框架**: Tkinter(Python 内置)
203 - **图片处理**: Pillow
204 - **API 客户端**: google-genai
205 - **数据库**: PyMySQL
206 - **打包工具**: PyInstaller
207
208 ## 获取 API 密钥
209
210 访问 [Google AI Studio](https://makersuite.google.com/app/apikey) 获取免费的 API 密钥。
211
212 ## 注意事项
213
214 - API 密钥会保存在 `config.json` 文件中,请妥善保管
215 - 使用 API 可能会产生费用,请查看 Google AI 的定价信息
216 - 生成高分辨率图片(4K)需要更多时间和 API 配额
217
218 ## 故障排查
219
220 ### 登录相关问题
221
222 **无法登录 / 数据库连接失败**
223 - 检查 `config.json` 中的 `db_config` 配置是否正确
224 - 确认数据库服务器可访问(检查防火墙/网络)
225 - 验证数据库用户名和密码是否正确
226 - 确认表 `nano_banana_users` 已创建
227
228 **"未找到数据库配置" 错误**
229 - 确保 `config.json` 包含 `db_config` 字段
230 - 参考快速开始章节的配置示例
231
232 **"用户名或密码错误" 提示**
233 - 使用 `python user_util.py list` 查看用户列表
234 - 确认用户状态为 'active'
235 - 使用 `user_util.py` 重置密码或创建新用户
236
237 **密码不匹配**
238 - 确保数据库中存储的是 SHA256 哈希值,而非明文密码
239 - 使用 `user_util.py add` 添加用户,会自动计算哈希
240
241 ### 配置文件只读错误(macOS/Windows 打包应用)
242 **问题**: 提示 "read-only file system: config.json"
243
244 **原因**: 打包后的应用资源目录是只读的,无法在应用包内写入文件
245
246 **解决方案**:
247 - ✅ 已修复:应用现在会自动将配置保存到用户目录
248 - macOS: `~/Library/Application Support/ZB100ImageGenerator/config.json`
249 - Windows: `%APPDATA%\ZB100ImageGenerator\config.json`
250 - 首次运行会自动创建配置文件夹和文件
251
252 ### 生成失败
253 - 检查 API 密钥是否正确
254 - 检查网络连接是否正常
255 - 查看错误信息,确认是否超出配额
256
257 ### 找不到 API 密钥
258 - 开发模式:检查项目目录下的 `config.json` 文件
259 - 打包应用:检查用户目录下的配置文件(见上方配置文件位置)
260 - 手动创建配置文件并添加完整配置(参考快速开始章节)
261
262 ### 打包失败
263 - 确保安装了所有依赖:`pip install -r requirements.txt`
264 - 检查 Python 版本是否符合要求(3.8+)
265 - Windows: 确保有 `zb100_kehuan.ico` 图标文件(或修改打包脚本移除 `--icon` 参数)
266 - 注意: `user_util.py` 不应打包进客户端版本(仅管理员使用)
267
268 ## 技术设计原则
269
270 本项目遵循 Linus Torvalds 的设计哲学:
271
272 - **简洁至上**: 使用 Tkinter 内置 GUI,避免重型框架
273 - **零特殊情况**: 统一的错误处理和数据流
274 - **实用主义**: 直接解决问题,不过度设计
275 - **清晰数据结构**: 简单的配置管理和图片数据流
276
277 ## 许可证
278
279 MIT License
1 # -*- mode: python ; coding: utf-8 -*-
2
3
4 a = Analysis(
5 ['image_generator.py'],
6 pathex=[],
7 binaries=[],
8 datas=[('config.json', '.')],
9 hiddenimports=[],
10 hookspath=[],
11 hooksconfig={},
12 runtime_hooks=[],
13 excludes=[],
14 noarchive=False,
15 optimize=0,
16 )
17 pyz = PYZ(a.pure)
18
19 exe = EXE(
20 pyz,
21 a.scripts,
22 a.binaries,
23 a.datas,
24 [],
25 name='ZB100ImageGenerator',
26 debug=False,
27 bootloader_ignore_signals=False,
28 strip=False,
29 upx=True,
30 upx_exclude=[],
31 runtime_tmpdir=None,
32 console=False,
33 disable_windowed_traceback=False,
34 argv_emulation=False,
35 target_arch=None,
36 codesign_identity=None,
37 entitlements_file=None,
38 icon=['zb100_kehuan.ico'],
39 )
1 #!/bin/bash
2 # macOS Build Script for Gemini Image Generator
3
4 echo "================================"
5 echo "Building Gemini Image Generator"
6 echo "================================"
7
8 # Check if virtual environment exists
9 if [ ! -d "venv" ]; then
10 echo "Creating virtual environment..."
11 python3 -m venv venv
12 fi
13
14 # Activate virtual environment
15 echo "Activating virtual environment..."
16 source venv/bin/activate
17
18 # Install dependencies
19 echo "Installing dependencies..."
20 pip install --upgrade pip
21 pip install -r requirements.txt
22
23 # Clean previous builds
24 echo "Cleaning previous builds..."
25 rm -rf build dist *.spec
26
27 # Build executable
28 echo "Building executable..."
29 pyinstaller --name="ZB100ImageGenerator" \
30 --onefile \
31 --windowed \
32 --add-data "config.json:." \
33 image_generator_qt.py
34
35 # Check if build was successful
36 if [ -f "dist/ZB100ImageGenerator.app/Contents/MacOS/ZB100ImageGenerator" ] || [ -f "dist/ZB100ImageGenerator" ]; then
37 echo "================================"
38 echo "Build successful!"
39 echo "Application: dist/ZB100ImageGenerator.app (or dist/ZB100ImageGenerator)"
40 echo "================================"
41 else
42 echo "================================"
43 echo "Build failed!"
44 echo "================================"
45 fi
1 @echo off
2 REM Windows Build Script for Gemini Image Generator
3
4 echo ================================
5 echo Building Gemini Image Generator
6 echo ================================
7
8 REM Check if virtual environment exists
9 if not exist "venv" (
10 echo Creating virtual environment...
11 python -m venv venv
12 )
13
14 REM Activate virtual environment
15 echo Activating virtual environment...
16 call venv\Scripts\activate.bat
17
18 REM Install dependencies
19 echo Installing dependencies...
20 pip install --upgrade pip
21 pip install -r requirements.txt
22
23 REM Clean previous builds
24 echo Cleaning previous builds...
25 if exist "build" rd /s /q build
26 if exist "dist" rd /s /q dist
27 if exist "*.spec" del /q *.spec
28
29 REM Build executable
30 echo Building executable...
31 pyinstaller --name="ZB100ImageGenerator" ^
32 --onefile ^
33 --windowed ^
34 --icon=zb100_kehuan.ico ^
35 --add-data "config.json;." ^
36 image_generator_qt.py
37
38 REM Check if build was successful
39 if exist "dist\ZB100ImageGenerator.exe" (
40 echo ================================
41 echo Build successful!
42 echo Executable: dist\ZB100ImageGenerator.exe
43 echo ================================
44 ) else (
45 echo ================================
46 echo Build failed!
47 echo ================================
48 )
49
50 pause
1 @echo off
2 REM 导出 Windows 环境的精确依赖版本
3
4 echo 正在导出依赖版本...
5
6 pip freeze | findstr /C:"google-genai" /C:"Pillow" /C:"PyQt5" /C:"pyinstaller" > requirements-lock.txt
7
8 echo.
9 echo 已导出到 requirements-lock.txt
10 echo 请将此文件复制到 Mac 上使用
11 echo.
12
13 type requirements-lock.txt
14
15 pause
1 #!/usr/bin/env python3
2 """
3 Gemini Image Generator App
4 Simple GUI application for generating images using Google's Gemini API
5 """
6
7 import tkinter as tk
8 from tkinter import ttk, filedialog, messagebox, scrolledtext
9 from PIL import Image, ImageTk
10 import base64
11 import io
12 import json
13 import os
14 import sys
15 import tempfile
16 import subprocess
17 import platform
18 from pathlib import Path
19 from google import genai
20 from google.genai import types
21 import threading
22 import hashlib
23 import pymysql
24
25
26 def hash_password(password: str) -> str:
27 """使用 SHA256 哈希密码"""
28 return hashlib.sha256(password.encode('utf-8')).hexdigest()
29
30
31 class DatabaseManager:
32 """数据库连接管理类"""
33 def __init__(self, db_config):
34 self.config = db_config
35
36 def authenticate(self, username, password):
37 """
38 验证用户凭证
39 返回: (success: bool, message: str)
40 """
41 try:
42 # 计算密码哈希
43 password_hash = hash_password(password)
44
45 # 连接数据库
46 conn = pymysql.connect(
47 host=self.config['host'],
48 port=self.config.get('port', 3306),
49 user=self.config['user'],
50 password=self.config['password'],
51 database=self.config['database'],
52 connect_timeout=5
53 )
54
55 try:
56 with conn.cursor() as cursor:
57 # 使用参数化查询防止 SQL 注入
58 sql = f"SELECT * FROM {self.config['table']} WHERE user_name=%s AND passwd=%s AND status='active'"
59 cursor.execute(sql, (username, password_hash))
60 result = cursor.fetchone()
61
62 if result:
63 return True, "认证成功"
64 else:
65 return False, "用户名或密码错误"
66 finally:
67 conn.close()
68
69 except pymysql.OperationalError as e:
70 return False, "无法连接到服务器,请检查网络连接"
71 except Exception as e:
72 return False, f"认证失败: {str(e)}"
73
74
75 class LoginWindow:
76 """登录窗口类"""
77 def __init__(self, db_config, last_user="", saved_password_hash=""):
78 self.db_config = db_config
79 self.last_user = last_user
80 self.saved_password_hash = saved_password_hash
81 self.success = False
82 self.authenticated_user = ""
83 self.password_changed = False # 标记密码是否被修改
84
85 # 创建登录窗口
86 self.root = tk.Tk()
87 self.root.title("登录 - AI 图像生成器")
88 self.root.geometry("400x400")
89 self.root.resizable(False, False)
90
91 # 创建 BooleanVar (必须在 Tk 根窗口创建之后)
92 self.remember_user = tk.BooleanVar(value=bool(last_user))
93 self.remember_password = tk.BooleanVar(value=bool(saved_password_hash))
94
95 # 设置窗口居中
96 self.center_window()
97
98 # 设置样式
99 self.setup_styles()
100
101 # 创建UI
102 self.setup_ui()
103
104 # 绑定回车键
105 self.root.bind('<Return>', lambda e: self.on_login())
106
107 # 处理窗口关闭
108 self.root.protocol("WM_DELETE_WINDOW", self.on_close)
109
110 def center_window(self):
111 """窗口居中显示"""
112 self.root.update_idletasks()
113 width = self.root.winfo_width()
114 height = self.root.winfo_height()
115 x = (self.root.winfo_screenwidth() // 2) - (width // 2)
116 y = (self.root.winfo_screenheight() // 2) - (height // 2)
117 self.root.geometry(f'{width}x{height}+{x}+{y}')
118
119 def setup_styles(self):
120 """设置样式"""
121 style = ttk.Style()
122 style.theme_use('clam')
123
124 bg_color = '#ffffff'
125 accent_color = '#007AFF'
126
127 self.root.configure(bg=bg_color)
128
129 style.configure('Login.TButton',
130 background=accent_color,
131 foreground='white',
132 borderwidth=0,
133 focuscolor='none',
134 font=('Segoe UI', 10, 'bold'),
135 padding=(20, 10))
136
137 style.map('Login.TButton',
138 background=[('active', '#0051D5'), ('pressed', '#0051D5')])
139
140 def setup_ui(self):
141 """创建登录界面"""
142 # 主容器
143 main_frame = ttk.Frame(self.root, padding=40)
144 main_frame.pack(fill="both", expand=True)
145
146 # 标题
147 title_label = tk.Label(main_frame, text="登录",
148 font=('Segoe UI', 20, 'bold'),
149 bg='white', fg='#1d1d1f')
150 title_label.pack(pady=(0, 30))
151
152 # 用户名
153 username_frame = ttk.Frame(main_frame)
154 username_frame.pack(fill="x", pady=(0, 15))
155
156 username_label = tk.Label(username_frame, text="用户名",
157 font=('Segoe UI', 10),
158 bg='white', fg='#666666')
159 username_label.pack(anchor="w")
160
161 self.username_entry = tk.Entry(username_frame,
162 font=('Segoe UI', 11),
163 relief='solid',
164 borderwidth=1)
165 self.username_entry.pack(fill="x", ipady=8, pady=(5, 0))
166 self.username_entry.insert(0, self.last_user)
167
168 # 密码
169 password_frame = ttk.Frame(main_frame)
170 password_frame.pack(fill="x", pady=(0, 15))
171
172 password_label = tk.Label(password_frame, text="密码",
173 font=('Segoe UI', 10),
174 bg='white', fg='#666666')
175 password_label.pack(anchor="w")
176
177 self.password_entry = tk.Entry(password_frame,
178 font=('Segoe UI', 11),
179 relief='solid',
180 borderwidth=1,
181 show='*')
182 self.password_entry.pack(fill="x", ipady=8, pady=(5, 0))
183
184 # 如果有保存的密码,显示占位符
185 if self.saved_password_hash:
186 self.password_entry.insert(0, "••••••••")
187 self.password_entry.config(fg='#999999')
188
189 # 监听密码框变化
190 self.password_entry.bind('<KeyPress>', self.on_password_change)
191 self.password_entry.bind('<Return>', lambda e: self.on_login())
192
193 # 复选框容器
194 checkbox_frame = ttk.Frame(main_frame)
195 checkbox_frame.pack(fill="x", pady=(0, 20))
196
197 # 记住用户名复选框
198 remember_user_check = tk.Checkbutton(checkbox_frame,
199 text="记住用户名",
200 variable=self.remember_user,
201 font=('Segoe UI', 9),
202 bg='white',
203 activebackground='white')
204 remember_user_check.pack(side="left")
205
206 # 记住密码复选框
207 remember_password_check = tk.Checkbutton(checkbox_frame,
208 text="记住密码",
209 variable=self.remember_password,
210 font=('Segoe UI', 9),
211 bg='white',
212 activebackground='white')
213 remember_password_check.pack(side="left", padx=(20, 0))
214
215 # 登录按钮
216 self.login_button = ttk.Button(main_frame,
217 text="登录",
218 style='Login.TButton',
219 command=self.on_login)
220 self.login_button.pack(fill="x")
221
222 # 错误提示标签
223 self.error_label = tk.Label(main_frame,
224 text="",
225 font=('Segoe UI', 9),
226 bg='white',
227 fg='#ff3b30')
228 self.error_label.pack(pady=(15, 0))
229
230 # 焦点设置
231 if self.last_user:
232 self.password_entry.focus()
233 else:
234 self.username_entry.focus()
235
236 def on_password_change(self, event):
237 """监听密码框变化"""
238 if not self.password_changed and self.saved_password_hash:
239 # 首次修改密码,清空占位符
240 self.password_entry.delete(0, tk.END)
241 self.password_entry.config(fg='#000000')
242 self.password_changed = True
243
244 def on_login(self):
245 """处理登录"""
246 print("[DEBUG] 登录按钮被点击")
247 username = self.username_entry.get().strip()
248 password_input = self.password_entry.get()
249
250 print(f"[DEBUG] 用户名: {username}")
251 print(f"[DEBUG] 密码输入长度: {len(password_input)}")
252 print(f"[DEBUG] 密码已修改: {self.password_changed}")
253 print(f"[DEBUG] 有保存的哈希: {bool(self.saved_password_hash)}")
254
255 # 验证输入
256 if not username:
257 print("[DEBUG] 用户名为空")
258 self.show_error("请输入用户名")
259 return
260
261 if not password_input:
262 print("[DEBUG] 密码为空")
263 self.show_error("请输入密码")
264 return
265
266 # 禁用按钮,防止重复点击
267 self.login_button.config(state='disabled')
268 self.error_label.config(text="正在验证...", fg='#666666')
269 self.root.update()
270
271 # 判断使用保存的密码还是新输入的密码
272 if not self.password_changed and self.saved_password_hash:
273 # 使用保存的密码哈希
274 print("[DEBUG] 使用保存的密码哈希")
275 password_hash = self.saved_password_hash
276 else:
277 # 计算新密码的哈希
278 print("[DEBUG] 计算新密码的哈希")
279 password_hash = hash_password(password_input)
280
281 # 直接使用哈希值进行数据库验证
282 try:
283 print(f"[DEBUG] 开始连接数据库: {self.db_config['host']}")
284 conn = pymysql.connect(
285 host=self.db_config['host'],
286 port=self.db_config.get('port', 3306),
287 user=self.db_config['user'],
288 password=self.db_config['password'],
289 database=self.db_config['database'],
290 connect_timeout=5
291 )
292 print("[DEBUG] 数据库连接成功")
293
294 try:
295 with conn.cursor() as cursor:
296 sql = f"SELECT * FROM {self.db_config['table']} WHERE user_name=%s AND passwd=%s AND status='active'"
297 print(f"[DEBUG] 执行查询,用户名: {username}, 哈希前8位: {password_hash[:8]}...")
298 cursor.execute(sql, (username, password_hash))
299 result = cursor.fetchone()
300 print(f"[DEBUG] 查询结果: {'找到用户' if result else '未找到匹配'}")
301
302 if result:
303 print("[DEBUG] 登录成功")
304 self.success = True
305 self.authenticated_user = username
306 # 保存密码哈希用于下次登录
307 self.current_password_hash = password_hash
308 self.root.quit()
309 self.root.destroy()
310 else:
311 print("[DEBUG] 用户名或密码错误")
312 self.show_error("用户名或密码错误")
313 self.password_entry.delete(0, tk.END)
314 self.password_changed = False
315 self.login_button.config(state='normal')
316 finally:
317 conn.close()
318 print("[DEBUG] 数据库连接已关闭")
319
320 except pymysql.OperationalError as e:
321 print(f"[DEBUG] 数据库连接失败: {e}")
322 self.show_error("无法连接到服务器,请检查网络连接")
323 self.login_button.config(state='normal')
324 except Exception as e:
325 print(f"[DEBUG] 认证异常: {e}")
326 self.show_error(f"认证失败: {str(e)}")
327 self.login_button.config(state='normal')
328
329 def show_error(self, message):
330 """显示错误信息"""
331 self.error_label.config(text=message, fg='#ff3b30')
332
333 def on_close(self):
334 """处理窗口关闭"""
335 self.success = False
336 self.root.quit()
337 self.root.destroy()
338
339 def run(self):
340 """运行登录窗口"""
341 self.root.mainloop()
342 return (self.success,
343 self.authenticated_user,
344 self.remember_user.get(),
345 self.remember_password.get(),
346 getattr(self, 'current_password_hash', ''))
347
348
349 class ImageGeneratorApp:
350 def __init__(self, root):
351 self.root = root
352 self.root.title("AI 图像生成器")
353 self.root.geometry("1200x850")
354 self.root.minsize(1000, 700)
355
356 # Configure modern styling
357 self.setup_styles()
358
359 self.api_key = ""
360 self.uploaded_images = [] # List of (file_path, PhotoImage) tuples
361 self.generated_image_data = None
362 self.generated_image_bytes = None
363 self.saved_prompts = [] # Store favorite prompts
364
365 self.load_config()
366 self.setup_ui()
367
368 def setup_styles(self):
369 """Setup modern macOS-inspired UI styles"""
370 style = ttk.Style()
371 style.theme_use('clam')
372
373 # macOS-inspired color palette
374 bg_color = '#ffffff'
375 secondary_bg = '#f6f6f6'
376 accent_color = '#007AFF'
377 hover_color = '#0051D5'
378 border_color = '#e5e5e5'
379 text_color = '#1d1d1f'
380
381 self.root.configure(bg=bg_color)
382
383 # Primary button style
384 style.configure('Accent.TButton',
385 background=accent_color,
386 foreground='white',
387 borderwidth=0,
388 focuscolor='none',
389 font=('Segoe UI', 10),
390 padding=(18, 8))
391
392 style.map('Accent.TButton',
393 background=[('active', hover_color), ('pressed', hover_color)],
394 foreground=[('disabled', '#999999')])
395
396 # Secondary button style
397 style.configure('Secondary.TButton',
398 background=secondary_bg,
399 foreground=text_color,
400 borderwidth=1,
401 relief='flat',
402 font=('Segoe UI', 9),
403 padding=(12, 6))
404
405 style.map('Secondary.TButton',
406 background=[('active', '#e8e8e8'), ('pressed', '#d8d8d8')])
407
408 # Icon button style (small, subtle)
409 style.configure('Icon.TButton',
410 background=bg_color,
411 foreground='#666666',
412 borderwidth=0,
413 font=('Segoe UI', 9),
414 padding=(4, 4))
415
416 style.map('Icon.TButton',
417 background=[('active', secondary_bg)],
418 foreground=[('active', accent_color)])
419
420 # Delete button style (visible with red hover)
421 style.configure('Delete.TButton',
422 background='#ff4444',
423 foreground='#ffffff',
424 borderwidth=1,
425 relief='solid',
426 font=('Segoe UI', 9, 'bold'),
427 padding=(3, 1))
428
429 style.map('Delete.TButton',
430 background=[('active', '#FF3B30'), ('pressed', '#cc0000')],
431 foreground=[('active', 'white'), ('pressed', 'white')],
432 borderwidth=[('active', 1)],
433 relief=[('active', 'solid')])
434
435 style.configure('TLabelframe', background=bg_color, borderwidth=0, relief='flat')
436 style.configure('TLabelframe.Label', background=bg_color, font=('Segoe UI', 10, 'bold'), foreground=text_color)
437 style.configure('TLabel', background=bg_color, font=('Segoe UI', 9), foreground=text_color)
438 style.configure('TFrame', background=bg_color)
439 style.configure('Card.TFrame', background=secondary_bg, relief='flat')
440 style.configure('TCombobox', font=('Segoe UI', 9))
441
442 def get_config_dir(self):
443 """Get the appropriate directory for config files based on platform and mode"""
444 # Check if running as a bundled app (PyInstaller)
445 if getattr(sys, 'frozen', False):
446 # Running as bundled app - use user data directory
447 system = platform.system()
448 if system == 'Darwin': # macOS
449 config_dir = Path.home() / 'Library' / 'Application Support' / 'ZB100ImageGenerator'
450 elif system == 'Windows':
451 config_dir = Path(os.getenv('APPDATA', Path.home())) / 'ZB100ImageGenerator'
452 else: # Linux and others
453 config_dir = Path.home() / '.config' / 'zb100imagegenerator'
454 else:
455 # Running in development mode - use current directory
456 config_dir = Path('.')
457
458 # Create directory if it doesn't exist
459 config_dir.mkdir(parents=True, exist_ok=True)
460 return config_dir
461
462 def get_config_path(self):
463 """Get the full path to config.json"""
464 return self.get_config_dir() / 'config.json'
465
466 def load_config(self):
467 """Load API key, saved prompts, and db config from config file"""
468 config_path = self.get_config_path()
469
470 # 初始化默认值
471 self.db_config = None
472 self.last_user = ""
473
474 # Try to load from user config first
475 if config_path.exists():
476 try:
477 with open(config_path, 'r', encoding='utf-8') as f:
478 config = json.load(f)
479 self.api_key = config.get("api_key", "")
480 self.saved_prompts = config.get("saved_prompts", [])
481 self.db_config = config.get("db_config")
482 self.last_user = config.get("last_user", "")
483 except Exception as e:
484 print(f"Failed to load config from {config_path}: {e}")
485
486 # If no config found and we're in bundled mode, try to load from bundled resources
487 if not self.api_key and getattr(sys, 'frozen', False):
488 try:
489 # PyInstaller creates a temp folder and stores path in _MEIPASS
490 bundle_dir = Path(sys._MEIPASS)
491 bundled_config = bundle_dir / 'config.json'
492 if bundled_config.exists():
493 with open(bundled_config, 'r', encoding='utf-8') as f:
494 config = json.load(f)
495 self.api_key = config.get("api_key", "")
496 self.db_config = config.get("db_config")
497 # Don't load saved_prompts from bundle, only API key
498 # Save to user config for future use
499 self.save_config()
500 except Exception as e:
501 print(f"Failed to load bundled config: {e}")
502
503 if not self.api_key:
504 messagebox.showwarning("警告", f"未找到API密钥\n配置文件位置: {config_path}\n\n请在应用中输入API密钥或手动编辑配置文件")
505
506 def save_config(self, last_user=None):
507 """Save configuration to file"""
508 config_path = self.get_config_path()
509
510 try:
511 config = {
512 "api_key": self.api_key,
513 "saved_prompts": self.saved_prompts
514 }
515
516 # 添加数据库配置(如果存在)
517 if self.db_config:
518 config["db_config"] = self.db_config
519
520 # 添加最后登录用户
521 if last_user is not None:
522 config["last_user"] = last_user
523 elif hasattr(self, 'last_user'):
524 config["last_user"] = self.last_user
525 else:
526 config["last_user"] = ""
527
528 # Ensure directory exists
529 config_path.parent.mkdir(parents=True, exist_ok=True)
530
531 with open(config_path, 'w', encoding='utf-8') as f:
532 json.dump(config, f, indent=2, ensure_ascii=False)
533 except Exception as e:
534 messagebox.showerror("错误", f"保存配置失败: {str(e)}\n路径: {config_path}")
535
536 def setup_ui(self):
537 """Setup the user interface with macOS-inspired design"""
538 # Main container
539 main_container = ttk.Frame(self.root, padding=20)
540 main_container.pack(fill="both", expand=True)
541
542 # Reference Images Section with preview
543 ref_frame = ttk.LabelFrame(main_container, text="参考图片", padding=12)
544 ref_frame.pack(fill="x", pady=(0, 15))
545
546 # Upload button and info
547 upload_header = ttk.Frame(ref_frame)
548 upload_header.pack(fill="x", pady=(0, 8))
549
550 upload_btn = ttk.Button(upload_header, text="+ 添加图片",
551 command=self.upload_images,
552 style='Secondary.TButton')
553 upload_btn.pack(side="left")
554 self.bind_hover_effect(upload_btn)
555
556 self.image_count_label = ttk.Label(upload_header, text="已选择 0 张", foreground='#666666')
557 self.image_count_label.pack(side="left", padx=12)
558
559 # Image preview container (horizontal scrollable)
560 preview_container = ttk.Frame(ref_frame, style='Card.TFrame', height=140)
561 preview_container.pack(fill="x", pady=(0, 0))
562 preview_container.pack_propagate(False)
563
564 # Canvas for horizontal scrolling (without visible scrollbar)
565 self.img_canvas = tk.Canvas(preview_container, height=110, bg='#f6f6f6',
566 highlightthickness=0, bd=0)
567 self.img_preview_frame = ttk.Frame(self.img_canvas, style='Card.TFrame')
568
569 self.img_preview_frame.bind("<Configure>",
570 lambda e: self.img_canvas.configure(scrollregion=self.img_canvas.bbox("all")))
571
572 self.img_canvas.create_window((0, 0), window=self.img_preview_frame, anchor="nw")
573
574 # Enable mouse wheel scrolling
575 self.img_canvas.bind('<MouseWheel>', self._on_mousewheel)
576 self.img_canvas.bind('<Shift-MouseWheel>', self._on_mousewheel)
577
578 self.img_canvas.pack(fill="both", expand=True)
579
580 # Content area: Prompt (left) + Settings (right)
581 content_row = ttk.Frame(main_container)
582 content_row.pack(fill="x", pady=(0, 15))
583
584 # Left: Prompt Section
585 prompt_container = ttk.LabelFrame(content_row, text="提示词", padding=12)
586 prompt_container.pack(side="left", fill="both", expand=True, padx=(0, 10))
587
588 # Prompt toolbar
589 prompt_toolbar = ttk.Frame(prompt_container)
590 prompt_toolbar.pack(fill="x", pady=(0, 8))
591
592 self.save_prompt_btn = ttk.Button(prompt_toolbar, text="⭐ 收藏",
593 command=self.save_prompt,
594 style='Icon.TButton')
595 self.save_prompt_btn.pack(side="left", padx=(0, 5))
596 self.bind_hover_effect(self.save_prompt_btn)
597
598 # Saved prompts dropdown
599 ttk.Label(prompt_toolbar, text="快速选择:", foreground='#666666').pack(side="left", padx=(10, 5))
600 self.saved_prompts_combo = ttk.Combobox(prompt_toolbar, width=30, state="readonly")
601 self.saved_prompts_combo.pack(side="left")
602 self.saved_prompts_combo.bind('<<ComboboxSelected>>', self.load_saved_prompt)
603 self.update_saved_prompts_list()
604
605 # Delete saved prompt button
606 delete_prompt_btn = ttk.Button(prompt_toolbar, text="🗑️ 删除",
607 command=self.delete_saved_prompt,
608 style='Icon.TButton')
609 delete_prompt_btn.pack(side="left", padx=(5, 0))
610 self.bind_hover_effect(delete_prompt_btn)
611
612 # Prompt text area
613 self.prompt_text = scrolledtext.ScrolledText(prompt_container, height=8, wrap=tk.WORD,
614 font=('Segoe UI', 10),
615 borderwidth=1, relief='solid',
616 bg='#fafafa')
617 self.prompt_text.pack(fill="both", expand=True)
618 self.prompt_text.insert("1.0", "一幅美丽的风景画,有山有湖,日落时分")
619
620 # Right: Settings Section
621 settings_container = ttk.LabelFrame(content_row, text="生成设置", padding=12)
622 settings_container.pack(side="right", fill="y")
623
624 # Aspect Ratio
625 ttk.Label(settings_container, text="宽高比", foreground='#666666').pack(anchor="w", pady=(0, 4))
626 self.aspect_ratio = ttk.Combobox(settings_container, width=18, state="readonly")
627 self.aspect_ratio['values'] = ("1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9")
628 self.aspect_ratio.current(0)
629 self.aspect_ratio.pack(fill="x", pady=(0, 12))
630
631 # Image Size
632 ttk.Label(settings_container, text="图片尺寸", foreground='#666666').pack(anchor="w", pady=(0, 4))
633 self.image_size = ttk.Combobox(settings_container, width=18, state="readonly")
634 self.image_size['values'] = ("1K", "2K", "4K")
635 self.image_size.current(1)
636 self.image_size.pack(fill="x", pady=(0, 12))
637
638 # Model
639 ttk.Label(settings_container, text="AI 模型", foreground='#666666').pack(anchor="w", pady=(0, 4))
640 self.model = ttk.Combobox(settings_container, width=18, state="readonly")
641 self.model['values'] = ("gemini-3-pro-image-preview")
642 self.model.current(0)
643 self.model.pack(fill="x", pady=(0, 0))
644
645 # Action buttons
646 action_frame = ttk.Frame(main_container)
647 action_frame.pack(fill="x", pady=(0, 15))
648
649 self.generate_btn = ttk.Button(action_frame, text="生成图片",
650 command=self.generate_image_async,
651 style='Accent.TButton')
652 self.generate_btn.pack(side="left", padx=(0, 10))
653 self.bind_hover_effect(self.generate_btn, scale=True)
654
655 self.download_btn = ttk.Button(action_frame, text="下载图片",
656 command=self.download_image,
657 state="disabled",
658 style='Secondary.TButton')
659 self.download_btn.pack(side="left", padx=(0, 10))
660 self.bind_hover_effect(self.download_btn)
661
662 self.status_label = ttk.Label(action_frame, text="● 就绪",
663 font=('Segoe UI', 9),
664 foreground='#007AFF')
665 self.status_label.pack(side="left", padx=15)
666
667 # Preview Section (expands to fill remaining space)
668 preview_frame = ttk.LabelFrame(main_container, text="预览", padding=12)
669 preview_frame.pack(fill="both", expand=True)
670
671 # Create a frame to center the image
672 preview_inner = ttk.Frame(preview_frame)
673 preview_inner.pack(fill="both", expand=True)
674
675 self.preview_label = ttk.Label(preview_inner, text="生成的图片将在这里显示\n双击用系统查看器打开",
676 anchor="center",
677 font=('Segoe UI', 10),
678 foreground='#999999')
679 self.preview_label.place(relx=0.5, rely=0.5, anchor="center")
680
681 # Bind double-click to open with system viewer
682 self.preview_label.bind('<Double-Button-1>', self.open_fullsize_view)
683
684 def _on_mousewheel(self, event):
685 """Handle horizontal scrolling with mouse wheel"""
686 # Shift+Wheel or just Wheel for horizontal scroll
687 self.img_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
688
689 def bind_hover_effect(self, widget, scale=False):
690 """Add smooth macOS-style hover effect"""
691 original_cursor = widget['cursor']
692
693 def on_enter(e):
694 widget['cursor'] = 'hand2'
695 # Subtle scale effect for primary buttons
696 if scale and hasattr(widget, 'configure'):
697 try:
698 widget.configure(padding=(19, 9))
699 except:
700 pass
701
702 def on_leave(e):
703 widget['cursor'] = original_cursor
704 if scale and hasattr(widget, 'configure'):
705 try:
706 widget.configure(padding=(18, 8))
707 except:
708 pass
709
710 widget.bind('<Enter>', on_enter)
711 widget.bind('<Leave>', on_leave)
712
713 def update_saved_prompts_list(self):
714 """Update the saved prompts dropdown"""
715 if self.saved_prompts:
716 # Show first 50 chars of each prompt
717 display_prompts = [p[:50] + "..." if len(p) > 50 else p for p in self.saved_prompts]
718 self.saved_prompts_combo['values'] = display_prompts
719 else:
720 self.saved_prompts_combo['values'] = []
721
722 def save_prompt(self):
723 """Save current prompt to favorites"""
724 prompt = self.prompt_text.get("1.0", tk.END).strip()
725 if not prompt:
726 self.status_label.config(text="● 提示词不能为空", foreground='#FF3B30')
727 return
728
729 if prompt in self.saved_prompts:
730 self.status_label.config(text="● 该提示词已收藏", foreground='#FF9500')
731 self.save_prompt_btn.config(text="✓ 已收藏")
732 # 2秒后恢复按钮文本
733 self.root.after(2000, lambda: self.save_prompt_btn.config(text="⭐ 收藏"))
734 return
735
736 self.saved_prompts.append(prompt)
737 self.save_config()
738 self.update_saved_prompts_list()
739 self.status_label.config(text="● 已收藏提示词", foreground='#34C759')
740 self.save_prompt_btn.config(text="✓ 已收藏")
741 # 2秒后恢复按钮文本
742 self.root.after(2000, lambda: self.save_prompt_btn.config(text="⭐ 收藏"))
743
744 def load_saved_prompt(self, event):
745 """Load a saved prompt"""
746 index = self.saved_prompts_combo.current()
747 if index >= 0 and index < len(self.saved_prompts):
748 self.prompt_text.delete("1.0", tk.END)
749 self.prompt_text.insert("1.0", self.saved_prompts[index])
750 self.status_label.config(text="● 已加载提示词", foreground='#007AFF')
751
752 def delete_saved_prompt(self):
753 """Delete the currently selected saved prompt"""
754 index = self.saved_prompts_combo.current()
755
756 if index < 0 or index >= len(self.saved_prompts):
757 self.status_label.config(text="● 请先选择要删除的提示词", foreground='#FF9500')
758 return
759
760 # Delete without confirmation - just do it
761 self.saved_prompts.pop(index)
762 self.save_config()
763 self.update_saved_prompts_list()
764 self.saved_prompts_combo.set('') # Clear selection
765 self.status_label.config(text="● 已删除提示词", foreground='#34C759')
766
767 def upload_images(self):
768 """Upload reference images with preview"""
769 files = filedialog.askopenfilenames(
770 title="选择参考图片",
771 filetypes=[("图片文件", "*.png *.jpg *.jpeg *.gif *.bmp"), ("所有文件", "*.*")]
772 )
773
774 if files:
775 for file_path in files:
776 try:
777 # Load and create thumbnail with uniform size
778 img = Image.open(file_path)
779
780 # Create 100x100 square thumbnail with center crop
781 thumb_size = 100
782 img_copy = img.copy()
783
784 # Calculate crop box for center crop
785 width, height = img_copy.size
786 aspect = width / height
787
788 if aspect > 1: # Landscape
789 new_width = int(height * 1)
790 left = (width - new_width) // 2
791 crop_box = (left, 0, left + new_width, height)
792 elif aspect < 1: # Portrait
793 new_height = int(width * 1)
794 top = (height - new_height) // 2
795 crop_box = (0, top, width, top + new_height)
796 else: # Square
797 crop_box = (0, 0, width, height)
798
799 # Crop to square and resize
800 img_square = img_copy.crop(crop_box)
801 img_square = img_square.resize((thumb_size, thumb_size), Image.Resampling.LANCZOS)
802 photo = ImageTk.PhotoImage(img_square)
803
804 # Add to list maintaining order
805 self.uploaded_images.append((file_path, photo))
806 except Exception as e:
807 messagebox.showerror("错误", f"无法加载图片: {file_path}\n{str(e)}")
808
809 self.update_image_preview()
810 self.image_count_label.config(text=f"已选择 {len(self.uploaded_images)} 张")
811 self.status_label.config(text=f"● 已添加 {len(files)} 张参考图片", foreground='#34C759')
812
813 def update_image_preview(self):
814 """Update the image preview panel"""
815 # Clear existing previews
816 for widget in self.img_preview_frame.winfo_children():
817 widget.destroy()
818
819 # Nothing to show if no images
820 if not self.uploaded_images:
821 return
822
823 # Add each image with delete button
824 for idx, (file_path, photo) in enumerate(self.uploaded_images):
825 # Container for each image
826 img_container = ttk.Frame(self.img_preview_frame, style='Card.TFrame')
827 img_container.pack(side="left", padx=5, pady=5)
828
829 # Store reference in container to prevent garbage collection
830 img_container._photo_ref = photo
831
832 # Image label
833 img_label = ttk.Label(img_container, image=photo, relief='solid', borderwidth=1)
834 img_label.image = photo # Keep reference
835 img_label.pack()
836
837 # Info frame (index + delete button)
838 info_frame = ttk.Frame(img_container, style='Card.TFrame')
839 info_frame.pack(fill="x", pady=(2, 0))
840
841 # Image index
842 index_label = ttk.Label(info_frame, text=f"图 {idx + 1}",
843 font=('Segoe UI', 8), foreground='#666666')
844 index_label.pack(side="left", padx=2)
845
846 # Delete button with enhanced visibility
847 del_btn = ttk.Button(info_frame, text="✕", width=3,
848 command=lambda i=idx: self.delete_image(i),
849 style='Delete.TButton')
850 del_btn.pack(side="right")
851
852 # Enhanced hover effect for delete button
853 def on_delete_hover(e, btn=del_btn):
854 btn['cursor'] = 'hand2'
855
856 def on_delete_leave(e, btn=del_btn):
857 btn['cursor'] = ''
858
859 del_btn.bind('<Enter>', on_delete_hover)
860 del_btn.bind('<Leave>', on_delete_leave)
861
862 # Force canvas to update scrollregion
863 self.img_canvas.update_idletasks()
864
865 def delete_image(self, index):
866 """Delete a specific image by index"""
867 if 0 <= index < len(self.uploaded_images):
868 self.uploaded_images.pop(index)
869 self.update_image_preview()
870 self.image_count_label.config(text=f"已选择 {len(self.uploaded_images)} 张")
871 self.status_label.config(text="● 已删除图片", foreground='#FF9500')
872
873
874
875 def image_to_base64(self, image_path):
876 """Convert image file to base64 string"""
877 with open(image_path, 'rb') as f:
878 return base64.b64encode(f.read()).decode('utf-8')
879
880 def generate_image_async(self):
881 """Start image generation in a separate thread"""
882 thread = threading.Thread(target=self.generate_image, daemon=True)
883 thread.start()
884
885 def generate_image(self):
886 """Generate image using Gemini API"""
887 prompt = self.prompt_text.get("1.0", tk.END).strip()
888
889 if not prompt:
890 self.root.after(0, lambda: messagebox.showerror("错误", "请输入图片描述!"))
891 return
892
893 if not self.api_key:
894 self.root.after(0, lambda: messagebox.showerror("错误", "未找到API密钥,请在config.json中配置!"))
895 return
896
897 self.root.after(0, lambda: self.status_label.config(text="● 正在生成图片...", foreground='#FF9500'))
898 self.root.after(0, lambda: self.generate_btn.config(state="disabled"))
899 self.root.after(0, lambda: self.download_btn.config(state="disabled"))
900
901 try:
902 client = genai.Client(api_key=self.api_key)
903
904 # Build content parts
905 content_parts = [prompt]
906
907 # Add reference images if uploaded
908 for img_path, _ in self.uploaded_images:
909 img_data = self.image_to_base64(img_path)
910 mime_type = "image/png"
911 if img_path.lower().endswith('.jpg') or img_path.lower().endswith('.jpeg'):
912 mime_type = "image/jpeg"
913
914 content_parts.append(
915 types.Part.from_bytes(
916 data=base64.b64decode(img_data),
917 mime_type=mime_type
918 )
919 )
920
921 # Generation config - using snake_case field names
922 config = types.GenerateContentConfig(
923 response_modalities=["IMAGE"],
924 image_config=types.ImageConfig(
925 aspect_ratio=self.aspect_ratio.get(),
926 image_size=self.image_size.get()
927 )
928 )
929
930 # Generate
931 response = client.models.generate_content(
932 model=self.model.get(),
933 contents=content_parts,
934 config=config
935 )
936
937 # Extract image - Fixed for proper data handling
938 for part in response.parts:
939 if hasattr(part, 'inline_data') and part.inline_data:
940 # Store both base64 string and raw bytes
941 if isinstance(part.inline_data.data, bytes):
942 self.generated_image_bytes = part.inline_data.data
943 self.generated_image_data = base64.b64encode(part.inline_data.data).decode('utf-8')
944 else:
945 self.generated_image_data = part.inline_data.data
946 self.generated_image_bytes = base64.b64decode(part.inline_data.data)
947
948 self.root.after(0, self.display_image)
949 self.root.after(0, lambda: self.download_btn.config(state="normal"))
950 self.root.after(0, lambda: self.status_label.config(text="● 图片生成成功", foreground='#34C759'))
951 return
952
953 raise Exception("响应中没有图片数据")
954
955 except Exception as e:
956 error_msg = str(e)
957 self.root.after(0, lambda: messagebox.showerror("错误", f"生成失败: {error_msg}"))
958 self.root.after(0, lambda: self.status_label.config(text="● 生成失败", foreground='#FF3B30'))
959 finally:
960 self.root.after(0, lambda: self.generate_btn.config(state="normal"))
961
962 def display_image(self):
963 """Display generated image in preview with proper scaling"""
964 if not self.generated_image_bytes:
965 return
966
967 try:
968 # Use raw bytes directly for display
969 image = Image.open(io.BytesIO(self.generated_image_bytes))
970
971 # Get available space (account for padding and labels)
972 preview_frame = self.preview_label.master
973 self.root.update_idletasks()
974 available_width = preview_frame.winfo_width() - 40
975 available_height = preview_frame.winfo_height() - 40
976
977 # Ensure minimum size
978 available_width = max(available_width, 400)
979 available_height = max(available_height, 300)
980
981 # Calculate scale to fit while maintaining aspect ratio
982 img_width, img_height = image.size
983 scale_w = available_width / img_width
984 scale_h = available_height / img_height
985 scale = min(scale_w, scale_h, 1.0) # Don't upscale
986
987 new_width = int(img_width * scale)
988 new_height = int(img_height * scale)
989
990 # Resize image
991 image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
992
993 photo = ImageTk.PhotoImage(image)
994 self.preview_label.config(image=photo, text="", cursor="hand2")
995 self.preview_label.image = photo
996 except Exception as e:
997 error_msg = str(e)
998 messagebox.showerror("错误", f"图片显示失败: {error_msg}")
999
1000 def open_fullsize_view(self, event=None):
1001 """Open generated image with system default viewer"""
1002 if not self.generated_image_bytes:
1003 return
1004
1005 try:
1006 # Create temporary file
1007 with tempfile.NamedTemporaryFile(delete=False, suffix='.png', mode='wb') as tmp_file:
1008 tmp_file.write(self.generated_image_bytes)
1009 tmp_path = tmp_file.name
1010
1011 # Open with system default viewer
1012 system = platform.system()
1013 if system == 'Windows':
1014 os.startfile(tmp_path)
1015 elif system == 'Darwin': # macOS
1016 subprocess.run(['open', tmp_path], check=True)
1017 else: # Linux and others
1018 subprocess.run(['xdg-open', tmp_path], check=True)
1019
1020 self.status_label.config(text="● 已用系统查看器打开", foreground='#007AFF')
1021
1022 except Exception as e:
1023 messagebox.showerror("错误", f"无法打开系统图片查看器: {str(e)}")
1024
1025 def download_image(self):
1026 """Download generated image"""
1027 if not self.generated_image_bytes:
1028 messagebox.showerror("错误", "没有可下载的图片!")
1029 return
1030
1031 file_path = filedialog.asksaveasfilename(
1032 defaultextension=".png",
1033 filetypes=[("PNG 文件", "*.png"), ("JPEG 文件", "*.jpg"), ("所有文件", "*.*")],
1034 title="保存图片"
1035 )
1036
1037 if file_path:
1038 try:
1039 # Use raw bytes directly for saving
1040 with open(file_path, 'wb') as f:
1041 f.write(self.generated_image_bytes)
1042
1043 file_size = len(self.generated_image_bytes)
1044 messagebox.showinfo("成功", f"图片已保存到:\n{file_path}\n\n文件大小: {file_size:,} 字节")
1045 self.status_label.config(text="● 图片已保存", foreground='#34C759')
1046 except Exception as e:
1047 messagebox.showerror("错误", f"保存失败: {str(e)}")
1048
1049
1050 def main():
1051 # 首先加载配置以获取数据库信息
1052 config_dir = Path('.')
1053 if getattr(sys, 'frozen', False):
1054 system = platform.system()
1055 if system == 'Darwin':
1056 config_dir = Path.home() / 'Library' / 'Application Support' / 'ZB100ImageGenerator'
1057 elif system == 'Windows':
1058 config_dir = Path(os.getenv('APPDATA', Path.home())) / 'ZB100ImageGenerator'
1059 else:
1060 config_dir = Path.home() / '.config' / 'zb100imagegenerator'
1061
1062 config_dir.mkdir(parents=True, exist_ok=True)
1063 config_path = config_dir / 'config.json'
1064
1065 db_config = None
1066 last_user = ""
1067 saved_password_hash = ""
1068
1069 if config_path.exists():
1070 try:
1071 with open(config_path, 'r', encoding='utf-8') as f:
1072 config = json.load(f)
1073 db_config = config.get("db_config")
1074 last_user = config.get("last_user", "")
1075 saved_password_hash = config.get("saved_password_hash", "")
1076 except Exception as e:
1077 print(f"Failed to load config: {e}")
1078
1079 # 如果没有数据库配置,显示错误并退出
1080 if not db_config:
1081 root = tk.Tk()
1082 root.withdraw()
1083 messagebox.showerror("配置错误",
1084 f"未找到数据库配置\n配置文件: {config_path}\n\n"
1085 "请确保 config.json 包含 db_config 字段")
1086 return
1087
1088 # 显示登录窗口
1089 login_window = LoginWindow(db_config, last_user, saved_password_hash)
1090 success, authenticated_user, remember_user, remember_password, password_hash = login_window.run()
1091
1092 # 如果登录失败,退出应用
1093 if not success:
1094 return
1095
1096 # 保存/清除 last_user 和密码哈希
1097 if config_path.exists():
1098 try:
1099 with open(config_path, 'r', encoding='utf-8') as f:
1100 config = json.load(f)
1101
1102 if remember_user:
1103 config["last_user"] = authenticated_user
1104 else:
1105 config["last_user"] = ""
1106
1107 if remember_password:
1108 config["saved_password_hash"] = password_hash
1109 else:
1110 config["saved_password_hash"] = ""
1111
1112 with open(config_path, 'w', encoding='utf-8') as f:
1113 json.dump(config, f, indent=2, ensure_ascii=False)
1114 except Exception as e:
1115 print(f"Failed to save config: {e}")
1116
1117 # 登录成功,启动主应用
1118 root = tk.Tk()
1119 app = ImageGeneratorApp(root)
1120 root.mainloop()
1121
1122
1123 if __name__ == "__main__":
1124 main()
1 # Qt GUI 版本依赖
2 # 核心依赖
3 google-genai>=1.0.0,<2.0.0
4 Pillow>=10.0.0,<11.0.0
5 PyQt5>=5.15.0,<6.0.0
6
7 # 数据库依赖
8 pymysql>=1.0.0,<2.0.0
9
10 # 打包工具
11 pyinstaller>=6.0.0,<7.0.0
1 #!/bin/bash
2 # Mac 环境设置脚本
3
4 echo "================================"
5 echo "Mac 环境设置 - Qt 版本"
6 echo "================================"
7
8 # 检查 Python 版本
9 python_version=$(python3 --version 2>&1)
10 echo "Python 版本: $python_version"
11
12 # 创建虚拟环境
13 if [ ! -d "venv" ]; then
14 echo "创建虚拟环境..."
15 python3 -m venv venv
16 fi
17
18 # 激活虚拟环境
19 echo "激活虚拟环境..."
20 source venv/bin/activate
21
22 # 升级 pip
23 echo "升级 pip..."
24 pip install --upgrade pip
25
26 # 卸载可能冲突的旧版本
27 echo "清理旧版本..."
28 pip uninstall -y google-genai Pillow PyQt5 pyinstaller 2>/dev/null
29
30 # 安装依赖
31 echo "================================"
32 echo "安装依赖..."
33 echo "================================"
34
35 # 如果有锁定版本文件,优先使用
36 if [ -f "requirements-lock.txt" ]; then
37 echo "使用锁定版本 (requirements-lock.txt)..."
38 pip install -r requirements-lock.txt
39 else
40 echo "使用范围版本 (requirements.txt)..."
41 pip install -r requirements.txt
42 fi
43
44 # 验证安装
45 echo "================================"
46 echo "验证安装..."
47 echo "================================"
48
49 pip list | grep -E "google-genai|Pillow|PyQt5|pyinstaller"
50
51 echo "================================"
52 echo "设置完成!"
53 echo "================================"
54 echo ""
55 echo "运行应用: python3 image_generator_qt.py"
56 echo ""
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 """
4 用户管理工具 - 管理员专用
5
6 用法:
7 python user_util.py add <username> <password> # 添加用户
8 python user_util.py list # 列出用户
9 python user_util.py disable <username> # 禁用用户
10 python user_util.py enable <username> # 启用用户
11 python user_util.py reset <username> <password> # 重置密码
12
13 安全提示:
14 - 此工具仅供管理员使用,请勿分发给用户
15 - 避免在命令行直接输入密码(可被 shell 历史记录)
16 - 建议使用环境变量或交互式输入密码
17 """
18
19 import hashlib
20 import pymysql
21 import json
22 import sys
23 import os
24 from pathlib import Path
25
26 # Windows 控制台编码修复
27 if sys.platform == 'win32':
28 import codecs
29 sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
30 sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
31
32
33 def hash_password(password: str) -> str:
34 """使用 SHA256 哈希密码"""
35 return hashlib.sha256(password.encode('utf-8')).hexdigest()
36
37
38 class UserManager:
39 """用户管理类"""
40 def __init__(self, db_config):
41 self.config = db_config
42
43 def add_user(self, username, password):
44 """添加新用户"""
45 hashed = hash_password(password)
46 conn = None
47 try:
48 conn = pymysql.connect(
49 host=self.config['host'],
50 port=self.config.get('port', 3306),
51 user=self.config['user'],
52 password=self.config['password'],
53 database=self.config['database'],
54 connect_timeout=5
55 )
56
57 with conn.cursor() as cursor:
58 sql = f"INSERT INTO {self.config['table']} (user_name, passwd, status) VALUES (%s, %s, %s)"
59 cursor.execute(sql, (username, hashed, 'active'))
60 conn.commit()
61 print(f"✓ 用户 '{username}' 添加成功")
62 print(f" 密码哈希: {hashed[:16]}...")
63
64 except pymysql.IntegrityError:
65 print(f"✗ 用户 '{username}' 已存在")
66 except pymysql.OperationalError as e:
67 print(f"✗ 数据库连接失败: {e}")
68 except Exception as e:
69 print(f"✗ 添加用户失败: {e}")
70 finally:
71 if conn:
72 conn.close()
73
74 def list_users(self):
75 """列出所有用户"""
76 conn = None
77 try:
78 conn = pymysql.connect(
79 host=self.config['host'],
80 port=self.config.get('port', 3306),
81 user=self.config['user'],
82 password=self.config['password'],
83 database=self.config['database'],
84 connect_timeout=5
85 )
86
87 with conn.cursor() as cursor:
88 sql = f"SELECT user_name, passwd, status FROM {self.config['table']}"
89 cursor.execute(sql)
90 results = cursor.fetchall()
91
92 if not results:
93 print("没有找到任何用户")
94 return
95
96 # 打印表格
97 print("\n用户列表:")
98 print(" ┌────────────────────────┬──────────┬──────────────────┐")
99 print(" │ 用户名 │ 状态 │ 密码哈希(前8位) │")
100 print(" ├────────────────────────┼──────────┼──────────────────┤")
101
102 for row in results:
103 username = row[0] or ""
104 passwd_hash = row[1] or ""
105 status = row[2] or "NULL"
106
107 # 填充空格使对齐
108 username_display = username[:20].ljust(20)
109 status_display = status[:8].ljust(8)
110 hash_display = passwd_hash[:16] if passwd_hash else "N/A".ljust(16)
111
112 print(f" │ {username_display} │ {status_display} │ {hash_display} │")
113
114 print(" └────────────────────────┴──────────┴──────────────────┘")
115 print(f"\n总计: {len(results)} 个用户\n")
116
117 except pymysql.OperationalError as e:
118 print(f"✗ 数据库连接失败: {e}")
119 except Exception as e:
120 print(f"✗ 查询用户失败: {e}")
121 finally:
122 if conn:
123 conn.close()
124
125 def disable_user(self, username):
126 """禁用用户"""
127 self._update_user_status(username, 'disabled', "已禁用")
128
129 def enable_user(self, username):
130 """启用用户"""
131 self._update_user_status(username, 'active', "已启用")
132
133 def _update_user_status(self, username, status, action_name):
134 """更新用户状态"""
135 conn = None
136 try:
137 conn = pymysql.connect(
138 host=self.config['host'],
139 port=self.config.get('port', 3306),
140 user=self.config['user'],
141 password=self.config['password'],
142 database=self.config['database'],
143 connect_timeout=5
144 )
145
146 with conn.cursor() as cursor:
147 sql = f"UPDATE {self.config['table']} SET status=%s WHERE user_name=%s"
148 affected = cursor.execute(sql, (status, username))
149
150 if affected > 0:
151 conn.commit()
152 print(f"✓ 用户 '{username}' {action_name}")
153 else:
154 print(f"✗ 用户 '{username}' 不存在")
155
156 except pymysql.OperationalError as e:
157 print(f"✗ 数据库连接失败: {e}")
158 except Exception as e:
159 print(f"✗ 更新用户状态失败: {e}")
160 finally:
161 if conn:
162 conn.close()
163
164 def reset_password(self, username, new_password):
165 """重置用户密码"""
166 hashed = hash_password(new_password)
167 conn = None
168 try:
169 conn = pymysql.connect(
170 host=self.config['host'],
171 port=self.config.get('port', 3306),
172 user=self.config['user'],
173 password=self.config['password'],
174 database=self.config['database'],
175 connect_timeout=5
176 )
177
178 with conn.cursor() as cursor:
179 sql = f"UPDATE {self.config['table']} SET passwd=%s WHERE user_name=%s"
180 affected = cursor.execute(sql, (hashed, username))
181
182 if affected > 0:
183 conn.commit()
184 print(f"✓ 用户 '{username}' 密码已重置")
185 print(f" 新密码哈希: {hashed[:16]}...")
186 else:
187 print(f"✗ 用户 '{username}' 不存在")
188
189 except pymysql.OperationalError as e:
190 print(f"✗ 数据库连接失败: {e}")
191 except Exception as e:
192 print(f"✗ 重置密码失败: {e}")
193 finally:
194 if conn:
195 conn.close()
196
197
198 def load_db_config():
199 """从 config.json 加载数据库配置"""
200 config_path = Path('config.json')
201
202 if not config_path.exists():
203 print(f"✗ 配置文件不存在: {config_path}")
204 print(f" 请确保在项目目录下运行此工具")
205 return None
206
207 try:
208 with open(config_path, 'r', encoding='utf-8') as f:
209 config = json.load(f)
210 db_config = config.get('db_config')
211
212 if not db_config:
213 print("✗ 配置文件中未找到 db_config 字段")
214 return None
215
216 required_fields = ['host', 'user', 'password', 'database', 'table']
217 missing = [f for f in required_fields if f not in db_config]
218
219 if missing:
220 print(f"✗ db_config 缺少必需字段: {', '.join(missing)}")
221 return None
222
223 return db_config
224
225 except json.JSONDecodeError:
226 print(f"✗ 配置文件格式错误: {config_path}")
227 return None
228 except Exception as e:
229 print(f"✗ 加载配置失败: {e}")
230 return None
231
232
233 def print_help():
234 """打印帮助信息"""
235 print(__doc__)
236
237
238 def main():
239 """主函数"""
240 if len(sys.argv) < 2:
241 print_help()
242 sys.exit(1)
243
244 command = sys.argv[1].lower()
245
246 # 加载数据库配置
247 db_config = load_db_config()
248 if not db_config:
249 sys.exit(1)
250
251 manager = UserManager(db_config)
252
253 # 处理命令
254 if command == 'add':
255 if len(sys.argv) != 4:
256 print("用法: python user_util.py add <username> <password>")
257 sys.exit(1)
258 username = sys.argv[2]
259 password = sys.argv[3]
260 manager.add_user(username, password)
261
262 elif command == 'list':
263 manager.list_users()
264
265 elif command == 'disable':
266 if len(sys.argv) != 3:
267 print("用法: python user_util.py disable <username>")
268 sys.exit(1)
269 username = sys.argv[2]
270 manager.disable_user(username)
271
272 elif command == 'enable':
273 if len(sys.argv) != 3:
274 print("用法: python user_util.py enable <username>")
275 sys.exit(1)
276 username = sys.argv[2]
277 manager.enable_user(username)
278
279 elif command == 'reset':
280 if len(sys.argv) != 4:
281 print("用法: python user_util.py reset <username> <password>")
282 sys.exit(1)
283 username = sys.argv[2]
284 new_password = sys.argv[3]
285 manager.reset_password(username, new_password)
286
287 else:
288 print(f"✗ 未知命令: {command}")
289 print_help()
290 sys.exit(1)
291
292
293 if __name__ == '__main__':
294 main()
No preview for this file type