kines9661 commited on
Commit
96db8f7
·
verified ·
1 Parent(s): 915c111

Upload 3 files

Browse files
Files changed (4) hide show
  1. .gitattributes +1 -0
  2. README.md +118 -0
  3. app.py +349 -0
  4. 螢幕擷取畫面 2025-09-25 041942.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ 螢幕擷取畫面[[:space:]]2025-09-25[[:space:]]041942.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 🚀 FLUX AI 終極版 - 一個強大的多模型 AI 圖像生成器
2
+
3
+ 這是一個基於 Streamlit 構建的、功能豐富的 AI 圖像生成 Web 應用。它不僅僅是一個簡單的界面,而是一個集成了**多 API 供應商**、**多模型支持**、**配置持久化**和**高級用戶體驗**的終極工具。
4
+
5
+ 這個項目的目標是提供一個統一、流暢且高度可配置的界面,讓用戶可以輕鬆地利用包括 FLUX 家族在內的各種頂級 AI 圖像生成模型進行創作。
6
+
7
+ ![image](https://github.com/kinai2028-dot/Flux-AI-Pro/blob/main/%E8%9E%A2%E5%B9%95%E6%93%B7%E5%8F%96%E7%95%AB%E9%9D%A2%202025-09-25%20041942.png)
8
+
9
+
10
+ <!-- 請替換為您自己的應用截圖 URL -->
11
+
12
+ ***
13
+
14
+ ## 🏆 核心功能
15
+
16
+ * **多 API 供應商支持**:
17
+ * 原生支持 **Pollinations.ai**、**NavyAI** 以及任何 **OpenAI 兼容**的 API 端點。
18
+ * 可在不同供應商之間無縫切換。
19
+
20
+ * **配置永久化 (`st.secrets`)**:
21
+ * 通過 Streamlit 的 `secrets` 功能實現 API 存檔的**永久保存**。配置一次,永久有效,應用重啟或重新部署後數據不會丟失。
22
+ * 即使沒有配置 Secrets,應用也能**健壯地啟動**,不會崩潰。
23
+
24
+ * **豐富的模型支持**:
25
+ * **手動擴充**支持最新的 **FLUX 模型家族**,包括 `flux-1.1-pro`, `flux.1-kontext-pro`, `flux.1-kontext-max`, `flux-dev`, 和 `flux-schnell`。
26
+ * 支持**自動模型發現**,可動態加載 API 端點支持的所有兼容模型。
27
+
28
+ * **批量生成**:
29
+ * 支持一次性生成**多張圖片**(最多 4 張),極大地提升了創作和篩選效率。
30
+ * 通過應用層並行請求,為不支持批量生成的 Pollinations.ai 實現了**無縫的多圖生成**體驗。
31
+
32
+ * **21 種藝術風格預設**:
33
+ * 內置從「電影感」、「賽博龐克」到「水墨畫」、「黑白線條藝術」等 **21 種**精心調校的藝術風格,一鍵應用。
34
+
35
+ * **流暢的 API 管理**:
36
+ * 在側邊欄提供了直觀的 UI,可以**新增、刪除、編輯** API 存檔。
37
+ * 編輯器具有**智能 URL 自動更新**功能,當您切換 API 供應商時,端點 URL 會自動填充為該供應商的預設值。
38
+
39
+ * **完整的用戶工作流**:
40
+ * **生成歷史**:自動保存最近的生成記錄,方便回溯和比較。
41
+ * **我的收藏**:一鍵收藏您喜歡的圖片。
42
+ * **圖像變體**:基於歷史或收藏中的任何一張圖片,可以一鍵「復用提示詞」來生成新的變體。
43
+
44
+ ## 🛠️ 技術棧
45
+
46
+ * **前端框架**: [Streamlit](https://streamlit.io/)
47
+ * **核心庫**: `openai`, `requests`, `Pillow`
48
+ * **推薦部署平台**: [Koyeb (免費方案)](https://www.koyeb.com/)
49
+
50
+ ***
51
+
52
+ ## 🚀 部署指南 (針對 Koyeb 免費方案)
53
+
54
+ 在 Koyeb 上部署此應用非常簡單,因為它對 Python 和 Streamlit 提供了出色的支持。
55
+
56
+ ### 1. 項目文件結構
57
+
58
+ 您的項目在根目錄下僅需包含兩個文件:
59
+
60
+ ```
61
+ .
62
+ ├── app.py # 主應用程式碼
63
+ └── requirements.txt # Python 依賴
64
+ ```
65
+
66
+ 您也可以選擇在本地創建 `.streamlit/secrets.toml` 文件用於開發測試。
67
+
68
+ ### 2. 文件內容
69
+
70
+ * **`app.py`**:
71
+ * 使用我們在對話中確認的**「終極模型版」**完整程式碼。
72
+
73
+ * **`requirements.txt`**:
74
+ ```
75
+ streamlit
76
+ openai
77
+ requests
78
+ Pillow
79
+ ```
80
+
81
+ ### 3. Koyeb 部署步驟
82
+ [![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?name=koyeb-flux-free&type=git&repository=kinai2028-dot%2FFlux-AI-Pro&branch=main&run_command=streamlit+run+app.py+--server.port%3D%24PORT+--server.address%3D0.0.0.0+--server.headless%3Dtrue&instance_type=free&regions=was&instances_min=0&autoscaling_sleep_idle_delay=300)
83
+ 1. **推送到 GitHub**: 將包含以上兩個文件的項目文件夾推送到一個新的或現有的 GitHub 儲存庫。
84
+ 2. **登錄 Koyeb**: 使用您的 GitHub 帳戶登錄 Koyeb。
85
+ 3. **創建 Web Service**:
86
+ * 在 Koyeb 儀表板上,點擊「**Create Service**」,然後選擇「**Web Service**」。
87
+ * 選擇 **GitHub** 作為部署方式,並選擇您的儲存庫。
88
+ 4. **配置服務 (關鍵步驟)**:
89
+ * Koyeb 會自動檢測到 `requirements.txt` 文件,並將其識別為 Python 項目。
90
+ * 在 "Builder" 部分,您需要**覆蓋運行命令**。
91
+ * 點擊「**Run command**」字段旁邊的「**Override**」開關,並輸入以下命令:
92
+ ```bash
93
+ streamlit run app.py --server.port=$PORT
94
+ ```
95
+ *這是確保 Koyeb 能在正確的端口上啟動 Streamlit 服務的關鍵步驟。*
96
+ 5. **設置 Secrets (用於永久存檔)**:
97
+ * 為了啟用**永久存檔**功能,您必須設置環境變量。
98
+ * 在您服務的「Settings」標籤頁下,進入「**Environment Variables**」部分。
99
+ * 點擊「**Add Variable**」,然後選擇「**Secret**」。
100
+ * 將**名��� (Name)** 設置為 `STREAMLIT_SECRETS`。
101
+ * 在**值 (Value)** 中,粘貼您本地 `secrets.toml` 文件的**全部內容**。`secrets.toml` 示例如下:
102
+ ```toml
103
+ # 您本地 .streamlit/secrets.toml 文件的內容
104
+ [api_profiles.我的NavyAI]
105
+ provider = "NavyAI"
106
+ api_key = "sk-your-navy-api-key-here"
107
+ base_url = "https://api.navy/v1"
108
+ validated = true
109
+
110
+ [api_profiles.我的Pollinations]
111
+ provider = "Pollinations.ai"
112
+ base_url = "https://image.pollinations.ai"
113
+ validated = true
114
+ pollinations_auth_mode = "免費"
115
+ ```
116
+ 6. **部署與訪問**:
117
+ * 點擊「**Deploy**」按鈕。Koyeb 將開始構建和部署您的應用。
118
+ * 完成後,您將獲得一個公開的 `.koyeb.app` 網址。點擊它,即可訪問您功能完備的 AI 圖像生成器!
app.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from openai import OpenAI
3
+ from PIL import Image
4
+ import requests
5
+ from io import BytesIO
6
+ import datetime
7
+ import base64
8
+ from typing import Dict, List, Tuple
9
+ import time
10
+ import random
11
+ import json
12
+ import uuid
13
+ import os
14
+ import re
15
+ from urllib.parse import urlencode, quote
16
+ import gc
17
+ from streamlit.errors import StreamlitAPIException, StreamlitSecretNotFoundError
18
+
19
+ # 為免費方案設定限制
20
+ MAX_HISTORY_ITEMS = 15
21
+ MAX_FAVORITE_ITEMS = 30
22
+ MAX_BATCH_SIZE = 4
23
+
24
+ # 圖像尺寸預設
25
+ IMAGE_SIZES = {
26
+ "自定義...": "Custom", "1024x1024": "正方形 (1:1)", "1080x1080": "IG 貼文 (1:1)",
27
+ "1080x1350": "IG 縱向 (4:5)", "1080x1920": "IG Story (9:16)", "1200x630": "FB 橫向 (1.91:1)",
28
+ }
29
+
30
+ # 風格預設
31
+ STYLE_PRESETS = {
32
+ # 基礎風格
33
+ "無": "", "電影感": "cinematic, dramatic lighting, high detail, sharp focus, epic",
34
+ "動漫風": "anime, manga style, vibrant colors, clean line art, studio ghibli", "賽博龐克": "cyberpunk, neon lights, futuristic city, high-tech, Blade Runner",
35
+ # 藝術流派
36
+ "印象派": "impressionism, soft light, visible brushstrokes, Monet style", "超現實主義": "surrealism, dreamlike, bizarre, Salvador Dali style",
37
+ "普普藝術": "pop art, bold colors, comic book style, Andy Warhol", "水墨畫": "ink wash painting, traditional chinese art, minimalist, zen",
38
+ # 數位與遊戲風格
39
+ "3D 模型": "3d model, octane render, unreal engine, hyperdetailed, 4k", "像素藝術": "pixel art, 16-bit, retro gaming style, sprite sheet",
40
+ "低面建模": "low poly, simple shapes, vibrant color palette, isometric", "矢量圖": "vector art, flat design, clean lines, graphic illustration",
41
+ # 幻想與特定風格
42
+ "蒸汽龐克": "steampunk, victorian, gears, clockwork, intricate details", "黑暗奇幻": "dark fantasy, gothic, grim, lovecraftian horror, moody lighting",
43
+ "水彩畫": "watercolor painting, soft wash, blended colors, delicate", "剪紙藝術": "paper cut-out, layered paper, papercraft, flat shapes",
44
+ "奇幻藝術": "fantasy art, epic, detailed, magical, lord of the rings", "漫畫書": "comic book art, halftone dots, bold outlines, graphic novel style",
45
+ "線條藝術": "line art, monochrome, minimalist, clean lines", "霓虹龐克": "neon punk, fluorescent, glowing, psychedelic, vibrant",
46
+ "黑白線條藝術": "black and white line art, minimalist, clean vector, coloring book style",
47
+ }
48
+
49
+ def rerun_app():
50
+ if hasattr(st, 'rerun'): st.rerun()
51
+ elif hasattr(st, 'experimental_rerun'): st.experimental_rerun()
52
+ else: st.stop()
53
+
54
+ st.set_page_config(page_title="FLUX AI (終極模型版)", page_icon="🏆", layout="wide")
55
+
56
+ # **FIX**: Add the latest FLUX models to the hardcoded list
57
+ API_PROVIDERS = {
58
+ "Pollinations.ai": {
59
+ "name": "Pollinations.ai Studio",
60
+ "base_url_default": "https://image.pollinations.ai",
61
+ "icon": "🌸",
62
+ "hardcoded_models": {
63
+ "flux-1.1-pro": {"name": "Flux 1.1 Pro", "icon": "🏆"},
64
+ "flux.1-kontext-pro": {"name": "Flux.1 Kontext Pro", "icon": "🧠"},
65
+ "flux.1-kontext-max": {"name": "Flux.1 Kontext Max", "icon": "👑"},
66
+ "flux-dev": {"name": "Flux Dev", "icon": "🛠️"},
67
+ "flux-schnell": {"name": "Flux Schnell", "icon": "⚡"}
68
+ }
69
+ },
70
+ "NavyAI": {"name": "NavyAI", "base_url_default": "https://api.navy/v1", "icon": "⚓"},
71
+ "OpenAI Compatible": {"name": "OpenAI 兼容 API", "base_url_default": "https://api.openai.com/v1", "icon": "🤖"},
72
+ }
73
+
74
+ BASE_FLUX_MODELS = {"flux.1-schnell": {"name": "FLUX.1 Schnell", "icon": "⚡", "priority": 1}}
75
+
76
+ # --- 核心函數 ---
77
+ def init_session_state():
78
+ if 'api_profiles' not in st.session_state:
79
+ try: base_profiles = st.secrets.get("api_profiles", {})
80
+ except StreamlitSecretNotFoundError: base_profiles = {}
81
+ st.session_state.api_profiles = base_profiles.copy() if base_profiles else {"預設 Pollinations": {'provider': 'Pollinations.ai', 'api_key': '', 'base_url': 'https://image.pollinations.ai', 'validated': True, 'pollinations_auth_mode': '免費', 'pollinations_token': '', 'pollinations_referrer': ''}}
82
+ if 'active_profile_name' not in st.session_state or st.session_state.active_profile_name not in st.session_state.api_profiles:
83
+ st.session_state.active_profile_name = list(st.session_state.api_profiles.keys())[0] if st.session_state.api_profiles else ""
84
+ defaults = {'generation_history': [], 'favorite_images': [], 'discovered_models': {}}
85
+ for key, value in defaults.items():
86
+ if key not in st.session_state: st.session_state[key] = value
87
+
88
+ def get_active_config(): return st.session_state.api_profiles.get(st.session_state.active_profile_name, {})
89
+
90
+ def auto_discover_models(client, provider, base_url) -> Dict[str, Dict]:
91
+ discovered = {}
92
+ try:
93
+ if provider == "Pollinations.ai":
94
+ response = requests.get(f"{base_url}/models", timeout=10)
95
+ if response.ok:
96
+ models = response.json()
97
+ for model_name in models: discovered[model_name] = {"name": model_name.replace('-', ' ').title(), "icon": "🌸"}
98
+ else: st.warning(f"無法從 Pollinations 獲取模型列表: HTTP {response.status_code}")
99
+ elif client:
100
+ models = client.models.list().data
101
+ for model in models:
102
+ if 'flux' in model.id.lower() or 'kontext' in model.id.lower():
103
+ icon = "⚡" if 'flux' in model.id.lower() else "🧠"
104
+ discovered[model.id] = {"name": model.id.replace('-', ' ').replace('_', ' ').title(), "icon": icon}
105
+ except Exception as e: st.error(f"發現模型失敗: {e}")
106
+ return discovered
107
+
108
+ def merge_models() -> Dict[str, Dict]:
109
+ provider = get_active_config().get('provider')
110
+ if provider == 'Pollinations.ai':
111
+ discovered = st.session_state.get('discovered_models', {})
112
+ hardcoded = API_PROVIDERS['Pollinations.ai'].get('hardcoded_models', {})
113
+ return {**hardcoded, **discovered}
114
+ else: return {**BASE_FLUX_MODELS, **st.session_state.get('discovered_models', {})}
115
+
116
+ def validate_api_key(api_key: str, base_url: str, provider: str) -> Tuple[bool, str]:
117
+ if provider == "Pollinations.ai": return True, "Pollinations.ai 無需驗證"
118
+ try: OpenAI(api_key=api_key, base_url=base_url).models.list(); return True, "API 密鑰驗證成功"
119
+ except Exception as e: return False, f"API 驗證失敗: {e}"
120
+
121
+ def generate_images_with_retry(client, **params) -> Tuple[bool, any]:
122
+ provider = get_active_config().get('provider')
123
+ n_images = params.get("n", 1)
124
+
125
+ if provider == "Pollinations.ai":
126
+ generated_images = []
127
+ for i in range(n_images):
128
+ try:
129
+ current_params = params.copy()
130
+ current_params["seed"] = random.randint(0, 1000000)
131
+ prompt = current_params.get("prompt", "")
132
+ if (neg_prompt := current_params.get("negative_prompt")): prompt += f" --no {neg_prompt}"
133
+ width, height = str(current_params.get("size", "1024x1024")).split('x')
134
+ api_params = {k: v for k, v in {"model": current_params.get("model"), "width": width, "height": height, "seed": current_params.get("seed"), "nologo": current_params.get("nologo"), "private": current_params.get("private"), "enhance": current_params.get("enhance"), "safe": current_params.get("safe")}.items() if v}
135
+ cfg = get_active_config()
136
+ headers = {}
137
+ auth_mode = cfg.get('pollinations_auth_mode', '免費')
138
+ if auth_mode == '令牌' and cfg.get('pollinations_token'): headers['Authorization'] = f"Bearer {cfg['pollinations_token']}"
139
+ elif auth_mode == '域名' and cfg.get('pollinations_referrer'): headers['Referer'] = cfg['pollinations_referrer']
140
+ response = requests.get(f"{cfg['base_url']}/prompt/{quote(prompt)}?{urlencode(api_params)}", headers=headers, timeout=120)
141
+ if response.ok:
142
+ b64_json = base64.b64encode(response.content).decode()
143
+ image_obj = type('Image', (object,), {'b64_json': b64_json})
144
+ generated_images.append(image_obj)
145
+ else: st.warning(f"第 {i+1} 張圖片生成失敗: HTTP {response.status_code}")
146
+ except Exception as e:
147
+ st.warning(f"第 {i+1} 張圖片生成時出錯: {e}")
148
+ continue
149
+ if generated_images:
150
+ response_obj = type('Response', (object,), {'data': generated_images})
151
+ return True, response_obj
152
+ else: return False, "所有圖片生成均失敗。"
153
+ else:
154
+ try:
155
+ sdk_params = {"model": params.get("model"), "prompt": params.get("prompt"), "negative_prompt": params.get("negative_prompt"), "size": str(params.get("size")), "n": n_images, "response_format": "b64_json"}
156
+ sdk_params = {k: v for k, v in sdk_params.items() if v is not None and v != ""}
157
+ return True, client.images.generate(**sdk_params)
158
+ except Exception as e: return False, str(e)
159
+ return False, "未知錯誤。"
160
+
161
+ def add_to_history(prompt: str, negative_prompt: str, model: str, images: List[str], metadata: Dict):
162
+ history = st.session_state.generation_history
163
+ history.insert(0, {"id": str(uuid.uuid4()), "timestamp": datetime.datetime.now(), "prompt": prompt, "negative_prompt": negative_prompt, "model": model, "images": images, "metadata": metadata})
164
+ st.session_state.generation_history = history[:MAX_HISTORY_ITEMS]
165
+
166
+ def display_image_with_actions(b64_json: str, image_id: str, history_item: Dict):
167
+ try:
168
+ img_data = base64.b64decode(b64_json)
169
+ st.image(Image.open(BytesIO(img_data)), use_container_width=True)
170
+ col1, col2, col3 = st.columns(3)
171
+ with col1: st.download_button("📥 下載", img_data, f"flux_{image_id}.png", "image/png", key=f"dl_{image_id}", use_container_width=True)
172
+ with col2:
173
+ is_fav = any(fav['id'] == image_id for fav in st.session_state.favorite_images)
174
+ if st.button("⭐" if is_fav else "☆", key=f"fav_{image_id}", use_container_width=True, help="收藏/取消收藏"):
175
+ if is_fav: st.session_state.favorite_images = [f for f in st.session_state.favorite_images if f['id'] != image_id]
176
+ else: st.session_state.favorite_images.append({"id": image_id, "image_b64": b64_json, "timestamp": datetime.datetime.now(), "history_item": history_item})
177
+ rerun_app()
178
+ with col3:
179
+ if st.button("🎨 變體", key=f"vary_{image_id}", use_container_width=True, help="使用此提示生成變體"):
180
+ st.session_state.update({'vary_prompt': history_item['prompt'], 'vary_negative_prompt': history_item.get('negative_prompt', ''), 'vary_model': history_item['model']})
181
+ rerun_app()
182
+ except Exception as e: st.error(f"圖像顯示錯誤: {e}")
183
+
184
+ def init_api_client():
185
+ cfg = get_active_config()
186
+ if cfg and cfg.get('api_key') and cfg.get('provider') != "Pollinations.ai":
187
+ try: return OpenAI(api_key=cfg['api_key'], base_url=cfg['base_url'])
188
+ except Exception: return None
189
+ return None
190
+
191
+ def editor_provider_changed():
192
+ provider = st.session_state.editor_provider_selectbox
193
+ st.session_state.editor_base_url = API_PROVIDERS[provider]['base_url_default']
194
+ st.session_state.editor_api_key = ""
195
+
196
+ def load_profile_to_editor_state(profile_name):
197
+ config = st.session_state.api_profiles.get(profile_name, {})
198
+ provider = config.get('provider', 'Pollinations.ai')
199
+ st.session_state.editor_provider_selectbox = provider
200
+ st.session_state.editor_base_url = config.get('base_url', API_PROVIDERS.get(provider, {})['base_url_default'])
201
+ st.session_state.editor_api_key = config.get('api_key', '')
202
+ st.session_state.editor_auth_mode = config.get('pollinations_auth_mode', '免費')
203
+ st.session_state.editor_referrer = config.get('pollinations_referrer', '')
204
+ st.session_state.editor_token = config.get('pollinations_token', '')
205
+ st.session_state.profile_being_edited = profile_name
206
+
207
+ def show_api_settings():
208
+ st.subheader("⚙️ API 存檔管理")
209
+ profile_names = list(st.session_state.api_profiles.keys())
210
+ if not profile_names: st.warning("沒有可用的 API 存檔。請新增一個。")
211
+ active_profile_name = st.selectbox("活動存檔", profile_names, index=profile_names.index(st.session_state.get('active_profile_name')) if st.session_state.get('active_profile_name') in profile_names else 0)
212
+ if st.session_state.get('active_profile_name') != active_profile_name or 'profile_being_edited' not in st.session_state or st.session_state.profile_being_edited != active_profile_name:
213
+ st.session_state.active_profile_name = active_profile_name
214
+ load_profile_to_editor_state(active_profile_name)
215
+ st.session_state.discovered_models = {}
216
+ rerun_app()
217
+
218
+ col1, col2 = st.columns(2)
219
+ with col1:
220
+ if st.button("➕ 新增存檔", use_container_width=True):
221
+ new_name = "新存檔"; count = 1
222
+ while new_name in st.session_state.api_profiles: new_name = f"新存檔_{count}"; count += 1
223
+ st.session_state.api_profiles[new_name] = {'provider': 'Pollinations.ai', 'validated': False, 'base_url': API_PROVIDERS['Pollinations.ai']['base_url_default']}
224
+ st.session_state.active_profile_name = new_name
225
+ rerun_app()
226
+ with col2:
227
+ if st.button("🗑️ 刪除當前存檔", use_container_width=True, disabled=len(profile_names) <= 1 or not active_profile_name):
228
+ if active_profile_name:
229
+ del st.session_state.api_profiles[active_profile_name]
230
+ st.session_state.active_profile_name = list(st.session_state.api_profiles.keys())[0]
231
+ rerun_app()
232
+
233
+ if active_profile_name:
234
+ with st.expander("📝 編輯當前活動存檔", expanded=True):
235
+ st.text_input("存檔名稱", value=active_profile_name, key="editor_profile_name")
236
+ st.selectbox("API 提供商", list(API_PROVIDERS.keys()), key='editor_provider_selectbox', on_change=editor_provider_changed)
237
+ st.text_input("API 端點 URL", key='editor_base_url')
238
+ if st.session_state.editor_provider_selectbox == "Pollinations.ai":
239
+ st.radio("認證模式", ["免費", "域名", "令牌"], key='editor_auth_mode', horizontal=True)
240
+ st.text_input("應用域名 (Referrer)", key='editor_referrer', disabled=(st.session_state.editor_auth_mode != '域名'))
241
+ st.text_input("API 令牌 (Token)", key='editor_token', type="password", disabled=(st.session_state.editor_auth_mode != '令牌'))
242
+ else: st.text_input("API 密鑰", key='editor_api_key', type="password")
243
+
244
+ if st.button("💾 保存/更新存檔", type="primary"):
245
+ provider = st.session_state.editor_provider_selectbox
246
+ new_config = {'provider': provider, 'base_url': st.session_state.editor_base_url}
247
+ if provider == "Pollinations.ai":
248
+ new_config.update({'api_key': '', 'pollinations_auth_mode': st.session_state.editor_auth_mode, 'pollinations_referrer': st.session_state.editor_referrer, 'pollinations_token': st.session_state.editor_token})
249
+ else: new_config.update({'api_key': st.session_state.editor_api_key, 'pollinations_auth_mode': '免費', 'pollinations_referrer': '', 'pollinations_token': ''})
250
+ is_valid, msg = validate_api_key(new_config['api_key'], new_config['base_url'], new_config['provider'])
251
+ new_config['validated'] = is_valid
252
+ new_name = st.session_state.editor_profile_name
253
+ if new_name != active_profile_name: del st.session_state.api_profiles[active_profile_name]
254
+ st.session_state.api_profiles[new_name] = new_config
255
+ st.session_state.active_profile_name = new_name
256
+ st.success(f"存檔 '{new_name}' 已保存。")
257
+ time.sleep(1); rerun_app()
258
+
259
+ init_session_state()
260
+ client = init_api_client()
261
+ cfg = get_active_config()
262
+ api_configured = cfg and cfg.get('validated', False)
263
+
264
+ # --- 側邊欄 ---
265
+ with st.sidebar:
266
+ show_api_settings()
267
+ st.markdown("---")
268
+ if api_configured:
269
+ st.success(f"🟢 活動存檔: '{st.session_state.active_profile_name}'")
270
+ can_discover = (client is not None) or (cfg.get('provider') == "Pollinations.ai")
271
+ if st.button("🔍 發現模型", use_container_width=True, disabled=not can_discover):
272
+ with st.spinner("🔍 正在發現模型..."):
273
+ discovered = auto_discover_models(client, cfg['provider'], cfg['base_url'])
274
+ st.session_state.discovered_models = discovered
275
+ st.success(f"發現 {len(discovered)} 個模型!") if discovered else st.warning("未發現任何模型。")
276
+ time.sleep(1); rerun_app()
277
+ elif st.session_state.api_profiles: st.error(f"🔴 '{st.session_state.active_profile_name}' 未驗證")
278
+ st.markdown("---")
279
+ st.info(f"⚡ **免費版優化**\n- 歷史: {MAX_HISTORY_ITEMS}\n- 收藏: {MAX_FAVORITE_ITEMS}")
280
+
281
+ st.title("🏆 FLUX AI (終極模型版)")
282
+
283
+ # --- 主介面 ---
284
+ tab1, tab2, tab3 = st.tabs(["🚀 生成圖像", f"📚 歷史 ({len(st.session_state.generation_history)})", f"⭐ 收藏 ({len(st.session_state.favorite_images)})"])
285
+
286
+ with tab1:
287
+ if not api_configured: st.warning("⚠️ 請在側邊欄選擇一個已驗證的存檔,或新增一個。")
288
+ else:
289
+ all_models = merge_models()
290
+ if not all_models: st.warning("⚠️ 未發現任何模型。請點擊側邊欄的「發現模型」。")
291
+ else:
292
+ prompt_default = st.session_state.pop('vary_prompt', '')
293
+ neg_prompt_default = st.session_state.pop('vary_negative_prompt', '')
294
+ model_default_key = st.session_state.pop('vary_model', list(all_models.keys())[0])
295
+ model_default_index = list(all_models.keys()).index(model_default_key) if model_default_key in all_models else 0
296
+
297
+ sel_model = st.selectbox("模型:", list(all_models.keys()), index=model_default_index, format_func=lambda x: f"{all_models.get(x, {}).get('icon', '🤖')} {all_models.get(x, {}).get('name', x)}")
298
+ n_images = st.slider("生成數量", 1, MAX_BATCH_SIZE, 1)
299
+ selected_style = st.selectbox("🎨 風格預設:", list(STYLE_PRESETS.keys()))
300
+ prompt_val = st.text_area("✍️ 提示詞:", value=prompt_default, height=100, placeholder="一隻貓在日落下飛翔,電影感,高品質")
301
+ negative_prompt_val = st.text_area("🚫 負向提示詞:", value=neg_prompt_default, height=50, placeholder="模糊, 糟糕的解剖結構, 文字, 水印")
302
+ size_preset = st.selectbox("圖像尺寸", options=list(IMAGE_SIZES.keys()), format_func=lambda x: IMAGE_SIZES[x])
303
+ final_size_str = size_preset
304
+ if size_preset == "自定義...":
305
+ w, h = st.columns(2)
306
+ width = w.slider("寬度", 256, 2048, 1024, 64)
307
+ height = h.slider("高度", 256, 2048, 1024, 64)
308
+ final_size_str = f"{width}x{height}"
309
+
310
+ enhance, private, nologo, safe = False, False, False, False
311
+ if cfg.get('provider') == "Pollinations.ai":
312
+ with st.expander("🌸 Pollinations.ai 進階選項"):
313
+ enhance, private, nologo, safe = st.checkbox("增強提示詞", True), st.checkbox("私密模式", True), st.checkbox("移除標誌", True), st.checkbox("安全模式", False)
314
+
315
+ if st.button("🚀 生成圖像", type="primary", use_container_width=True, disabled=not prompt_val.strip()):
316
+ final_prompt = f"{prompt_val}, {STYLE_PRESETS[selected_style]}" if selected_style != "無" and STYLE_PRESETS[selected_style] else prompt_val
317
+ with st.spinner(f"🎨 正在生成 {n_images} 張圖像..."):
318
+ params = {"model": sel_model, "prompt": final_prompt, "negative_prompt": negative_prompt_val, "size": final_size_str, "n": n_images, "enhance": enhance, "private": private, "nologo": nologo, "safe": safe}
319
+ success, result = generate_images_with_retry(client, **params)
320
+ if success and result.data:
321
+ img_b64s = [img.b64_json for img in result.data]
322
+ add_to_history(prompt_val, negative_prompt_val, sel_model, img_b64s, {"size": final_size_str, "provider": cfg['provider'], "style": selected_style, "n": n_images})
323
+ st.success(f"✨ 成功生成 {len(img_b64s)} 張圖像!")
324
+ cols = st.columns(min(len(img_b64s), 2))
325
+ for i, b64_json in enumerate(img_b64s):
326
+ with cols[i % 2]: display_image_with_actions(b64_json, f"{st.session_state.generation_history[0]['id']}_{i}", st.session_state.generation_history[0])
327
+ gc.collect()
328
+ else: st.error(f"❌ 生成失敗: {result}")
329
+
330
+ with tab2:
331
+ if not st.session_state.generation_history: st.info("📭 尚無生成歷史。")
332
+ else:
333
+ for item in st.session_state.generation_history:
334
+ with st.expander(f"🎨 {item['prompt'][:50]}... | {item['timestamp'].strftime('%m-%d %H:%M')}"):
335
+ model_name = merge_models().get(item['model'], {}).get('name', item['model'])
336
+ st.markdown(f"**提示詞**: {item['prompt']}\n\n**模型**: {model_name}")
337
+ if item.get('negative_prompt'): st.markdown(f"**負向提示詞**: {item['negative_prompt']}")
338
+ cols = st.columns(min(len(item['images']), 2))
339
+ for i, b64_json in enumerate(item['images']):
340
+ with cols[i % 2]: display_image_with_actions(b64_json, f"hist_{item['id']}_{i}", item)
341
+
342
+ with tab3:
343
+ if not st.session_state.favorite_images: st.info("⭐ 尚無收藏的圖像。")
344
+ else:
345
+ cols = st.columns(3)
346
+ for i, fav in enumerate(sorted(st.session_state.favorite_images, key=lambda x: x['timestamp'], reverse=True)):
347
+ with cols[i % 3]: display_image_with_actions(fav['image_b64'], fav['id'], fav.get('history_item'))
348
+
349
+ st.markdown("""<div style="text-align: center; color: #888; margin-top: 2rem;"><small>🏆 終極模型版 | 部署在雲端平台 🏆</small></div>""", unsafe_allow_html=True)
螢幕擷取畫面 2025-09-25 041942.png ADDED

Git LFS Details

  • SHA256: b97ac2acf68e5d4167cfb722a8bbc96755eb553ed0d30ef442117c5214915cde
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB