Spaces:
Configuration error
Configuration error
Upload 3 files
Browse files- .gitattributes +1 -0
- README.md +118 -0
- app.py +349 -0
- 螢幕擷取畫面 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 |
+

|
| 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 |
+
[](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®ions=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
|