import streamlit as st import time import math import uuid import datetime import json import os import random import requests import urllib.parse # 引用配置与工具 from config import CUSTOM_CSS, PROVIDER_MAP, SIZE_PRESETS, QUALITY_PRESETS, STYLE_PRESETS, ITEMS_PER_PAGE, WARMUP_COUNT, LOCAL_CACHE_DIR, THUMBNAIL_DIR from utils import ensure_png_bytes, auto_translate # 引用拆分出的 UI 组件 (V30.11/12 新架构) from ui import ( get_managers, render_header, render_pagination, render_automation_dashboard, view_image_dialog, edit_webdav_dialog, login_page, change_pw_page ) # 页面配置 st.set_page_config( page_title="AI 艺术工作室 (旗舰版)", page_icon="🎨", layout="wide", initial_sidebar_state="collapsed" ) st.markdown(CUSTOM_CSS, unsafe_allow_html=True) # Session State 初始化 keys = ["current_record", "logged_in", "username", "must_change_pw", "prompt_input", "global_error", "upscale_error", "prompt_seed", "history_loaded", "poster_bytes", "auto_task_config", "style_preset", "gallery_page", "batch_upload_logs", "saved_tasks", "tokens_loaded", "selected_task_index", "last_mtime"] for k in keys: if k not in st.session_state: if "batch_upload_logs" == k: st.session_state[k] = [] elif "saved_tasks" == k: st.session_state[k] = [] elif "gallery_page" == k: st.session_state[k] = 1 # [Fix] 关键修复:last_mtime 必须初始化为 0.0,否则与浮点数比较会报错 elif "last_mtime" == k: st.session_state[k] = 0.0 else: st.session_state[k] = None # 初始化管理器 mgrs = get_managers() pl, tm, cg, sm, wm, atm, up, pg, am = mgrs["pl"], mgrs["tm"], mgrs["cg"], mgrs["sm"], mgrs["wm"], mgrs["atm"], mgrs["up"], mgrs["pg"], mgrs["am"] # 启动后台服务 if not st.session_state.history_loaded: sm.start_background_cacher(WARMUP_COUNT) st.session_state.history_loaded = True # --- 登录校验 --- if not st.session_state.get("logged_in", False): login_page() st.stop() if st.session_state.get("must_change_pw", False): change_pw_page() st.stop() # --- 主界面逻辑 --- render_header() # 顶部 Tab 导航 tab_main, tab_gallery, tab_batch, tab_settings = st.tabs(["🎨 创作工坊", "🖼️ 画廊归档", "🤖 自动化任务", "⚙️ 系统设置"]) # ==================== Tab 1: 创作工坊 ==================== with tab_main: c_side, c_canvas = st.columns([1, 2], gap="medium") with c_side: st.subheader("💡 灵感控制台") # [V30.40 Fix] 修复 get_best_token 调用错误,改为 get_active_tokens def on_random_click(): try: # 获取可用 Token 列表 active_tokens = tm.get_active_tokens() token = active_tokens[0] if active_tokens else None seed_text = random.choice(["Cyberpunk City", "Peaceful Garden", "Future Tech", "Magic Forest", "Portrait of a Lady"]) # 尝试调用文本生成接口 if token: u = f"https://gen.pollinations.ai/text/{urllib.parse.quote(f'Imagine: {seed_text}')}?model=openai&seed={random.randint(0,999)}&private=true&key={token}" try: resp = requests.get(u, timeout=5) if resp.status_code == 200: seed_text = resp.text except: pass st.session_state.prompt_input = seed_text except Exception as e: st.session_state.global_error = f"灵感生成失败: {str(e)}" # 提示词输入区 prompt = st.text_area("提示词 (Prompt)", value=st.session_state.get("prompt_input", ""), height=120, key="prompt_area") c_btn1, c_btn2 = st.columns([1, 1]) if c_btn1.button("🎲 随机灵感", use_container_width=True): on_random_click() st.rerun() if c_btn2.button("🧹 清空", use_container_width=True): st.session_state.prompt_input = "" st.rerun() # 参数设置 with st.expander("🛠️ 参数设置", expanded=True): c_p1, c_p2 = st.columns(2) # 尺寸选择 size_name = c_p1.selectbox("画幅比例", list(SIZE_PRESETS.keys()), index=1) width, height = SIZE_PRESETS[size_name] if size_name == "Custom (自定义)": c_cust1, c_cust2 = st.columns(2) width = c_cust1.number_input("宽", 64, 2048, 1024, step=64) height = c_cust2.number_input("高", 64, 2048, 1024, step=64) # 模型与风格 prov = c_p2.selectbox("模型供应商", list(PROVIDER_MAP.keys()), index=0) models = PROVIDER_MAP.get(prov, {}) model_key = st.selectbox("选择模型", list(models.keys()), format_func=lambda x: models[x]) # 风格与画质 style = st.selectbox("艺术风格", list(STYLE_PRESETS.keys()), index=0) quality = st.selectbox("画质增强", list(QUALITY_PRESETS.keys()), index=0) negative = st.text_input("反向提示词 (Negative)", value="") with c_canvas: # 错误提示 if st.session_state.global_error: st.error(f"❌ {st.session_state.global_error}") if st.button("清除错误"): st.session_state.global_error = None st.rerun() # 生成按钮区 c_gen_main, c_gen_status = st.columns([1, 3]) is_generating = False with c_gen_main: if st.button("🚀 开始生成", type="primary", use_container_width=True): if not prompt: st.warning("请先输入提示词") else: is_generating = True st.session_state.global_error = None # 预览与生成逻辑 preview_container = st.container(border=True) with preview_container: if is_generating: with st.status("🎨 正在绘制中...", expanded=True) as status: start_time = time.time() try: status.write("🔍 正在优化提示词...") # 自动翻译逻辑 final_prompt = prompt if any("\u4e00" <= char <= "\u9fff" for char in prompt): final_prompt = auto_translate(prompt) status.write(f"🌐 已翻译: {final_prompt[:30]}...") status.write("🖌️ 正在请求绘图引擎...") # [V30.38 Fix] 调用 generate 时传递 token_manager,不传具体 Key # 手动模式策略:balance (余额优先轮询) img_bytes, final_p, src_info = cg.generate( final_prompt, width, height, prov, model_key, style, quality, negative, token_manager=tm, strategy="balance" ) status.write("💾 正在保存作品...") record = { "id": uuid.uuid4().hex, "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), "prompt_zh": prompt, "prompt_en": final_p, "model": f"{src_info}", "style": style } sm.save_record(record, img_bytes) st.session_state.current_record = record st.session_state.poster_bytes = None # 重置海报 # 自动上传 WebDAV if wm.configs: status.write("☁️ 同步到 WebDAV...") wm.upload_to_all_nodes(img_bytes, f"manual_{record['id']}.png") status.update(label=f"✅ 完成! (耗时 {time.time()-start_time:.1f}s)", state="complete", expanded=False) st.rerun() except Exception as e: st.session_state.global_error = str(e) status.update(label="❌ 生成失败", state="error") # 显示当前作品 if st.session_state.current_record: rec = st.session_state.current_record local_path = os.path.join(LOCAL_CACHE_DIR, rec["img_path_in_repo"]) if os.path.exists(local_path): st.image(local_path, use_container_width=True) # 操作栏 c_act1, c_act2, c_act3 = st.columns(3) if c_act1.button("🔍 查看详情", use_container_width=True): with open(local_path, "rb") as f: view_image_dialog(f.read(), rec) if c_act2.button("🖼️ 生成海报", use_container_width=True): with open(local_path, "rb") as f: st.session_state.poster_bytes = pg.generate_social_poster(f.read(), rec) if c_act3.button("✨ 超分放大", use_container_width=True): with st.spinner("正在放大 2x..."): try: with open(local_path, "rb") as f: big_bytes = up.upscale_image(f.read()) new_rec = rec.copy() new_rec["id"] = uuid.uuid4().hex new_rec["timestamp"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") new_rec["model"] += " (Upscaled)" sm.save_record(new_rec, big_bytes) st.session_state.current_record = new_rec st.success("放大成功,已保存为新副本") time.sleep(1) st.rerun() except Exception as e: st.error(f"放大失败: {e}") # 海报展示区 if st.session_state.poster_bytes: st.divider() st.subheader("📱 社交海报预览") st.image(st.session_state.poster_bytes, width=360) st.download_button("⬇️ 下载海报", st.session_state.poster_bytes, file_name=f"poster_{rec['id']}.jpg", mime="image/jpeg") else: st.markdown("""
🎨
在左侧输入提示词,开始您的创作之旅
""", unsafe_allow_html=True) # ==================== Tab 2: 画廊归档 ==================== with tab_gallery: col_gal_head, col_gal_search = st.columns([3, 1]) with col_gal_head: st.subheader("📂 历史作品库") # 获取数据 all_history = sm.load_index() if not all_history: st.info("暂无历史记录") else: # 分页逻辑 total_items = len(all_history) total_pages = math.ceil(total_items / ITEMS_PER_PAGE) current_page = st.session_state.gallery_page start_idx = (current_page - 1) * ITEMS_PER_PAGE end_idx = start_idx + ITEMS_PER_PAGE page_items = all_history[start_idx:end_idx] # 渲染九宫格 rows = [page_items[i:i+3] for i in range(0, len(page_items), 3)] for row in rows: cols = st.columns(3) for idx, item in enumerate(row): with cols[idx]: img_path = item.get("img_path_in_repo", f"images/{item['id']}.png") # 优先加载本地缩略图 > 本地原图 > 远程 thumb_path = os.path.join(THUMBNAIL_DIR, f"{item['id']}.jpg") local_full = os.path.join(LOCAL_CACHE_DIR, img_path) display_img = None if os.path.exists(thumb_path): display_img = thumb_path elif os.path.exists(local_full): display_img = local_full if display_img: st.markdown(f'', unsafe_allow_html=True) b1, b2 = st.columns([3, 1]) if b1.button(f"👁️ {item['prompt_zh'][:10]}...", key=f"v_{item['id']}"): # 点击查看详情时,确保原图存在 full_data = sm.download_image(img_path) if full_data: view_image_dialog(full_data, item) else: st.toast("正在从云端下载原图,请稍后...", icon="☁️") if b2.button("🗑️", key=f"d_{item['id']}"): # 暂未实现物理删除,仅演示 st.toast("删除功能开发中", icon="🚧") else: st.markdown("""
☁️ 图片同步中...
""", unsafe_allow_html=True) # 触发后台下载 sm.ensure_image_on_disk(img_path, item['id']) render_pagination(total_pages, "gal_main") # ==================== Tab 3: 自动化任务 ==================== with tab_batch: render_automation_dashboard() # ==================== Tab 4: 系统设置 ==================== with tab_settings: st.subheader("🔑 Token 管理 (Pollinations)") # Token 列表管理 if st.session_state.get("tokens_loaded") != tm.last_save_time: st.session_state.tokens_loaded = tm.last_save_time with st.container(border=True): c_add1, c_add2, c_add3 = st.columns([2, 4, 1]) new_alias = c_add1.text_input("备注名", placeholder="例如: MyKey_01") new_key = c_add2.text_input("Token Key", placeholder="pollinations_sk_...", type="password") if c_add3.button("➕ 添加", use_container_width=True): if new_alias and new_key: tm.add_token(new_alias, new_key) st.success("添加成功") time.sleep(1) st.rerun() st.divider() # Token 状态展示 for idx, t in enumerate(tm.tokens): c1, c2, c3, c4 = st.columns([2, 2, 2, 1]) c1.markdown(f"**{t['alias']}**") # 状态指示灯 status_map = { "active": "🟢 正常", "exhausted": "🔴 余额不足", "invalid": "🚫 无效/被封" } c2.markdown(status_map.get(t['status'], "⚪ 未知")) c3.markdown(f"💰 `${t.get('balance', 0.0):.4f}`") if c4.button("🗑️", key=f"del_t_{idx}"): tm.delete_token(idx) st.rerun() st.subheader("☁️ WebDAV 备份节点") with st.expander("添加新节点", expanded=False): with st.form("add_webdav"): n = st.text_input("节点名称") h = st.text_input("服务器地址 (URL)") u = st.text_input("账号") p = st.text_input("密码", type="password") r = st.text_input("根路径", value="/") en = st.checkbox("立即启用", value=True) if st.form_submit_button("保存"): wm.add_connection(n, h, u, p, r, en) st.rerun() if wm.configs: for c in wm.configs: with st.container(border=True): c1, c2, c3 = st.columns([0.5, 2.5, 1]) is_enabled = c.get("enabled", True) c1.markdown("🟢" if is_enabled is True else "⚪") c2.markdown(f"**{c['name']}**") cc1, cc2 = c3.columns(2) if cc1.button("✏️", key=f"ed_{c['id']}"): edit_webdav_dialog(c) if cc2.button("🗑️", key=f"dl_{c['id']}"): wm.delete_connection(c['id']) st.rerun() st.divider() st.subheader("🔐 安全") with st.expander("修改密码"): with st.form("pw"): p1 = st.text_input("新密码", type="password") p2 = st.text_input("确认", type="password") if st.form_submit_button("修改"): if p1==p2: mgrs["am"].update_password(st.session_state.username, p1) st.success("成功") time.sleep(1) st.rerun() else: st.error("不一致")