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("""