Files
AI_NovelGenerator/ui/main_window.py
T
PenBo1andGitHub 9ec873600d Add files via upload
增加代理设置选项;
优化新版本google-generativeai 不支持 Client 类问题;
优化批量生成章节界面,使用customtkinter统一设计风格;
2025-09-08 11:58:44 +08:00

410 lines
18 KiB
Python

# ui/main_window.py
# -*- coding: utf-8 -*-
import os
import threading
import logging
import traceback
import customtkinter as ctk
import tkinter as tk
from tkinter import filedialog, messagebox
from .role_library import RoleLibrary
from llm_adapters import create_llm_adapter
from config_manager import load_config, save_config, test_llm_config, test_embedding_config
from utils import read_file, save_string_to_txt, clear_file_content
from tooltips import tooltips
from ui.context_menu import TextWidgetContextMenu
from ui.main_tab import build_main_tab, build_left_layout, build_right_layout
from ui.config_tab import build_config_tabview, load_config_btn, save_config_btn
from ui.novel_params_tab import build_novel_params_area, build_optional_buttons_area
from ui.generation_handlers import (
generate_novel_architecture_ui,
generate_chapter_blueprint_ui,
generate_chapter_draft_ui,
finalize_chapter_ui,
do_consistency_check,
import_knowledge_handler,
clear_vectorstore_handler,
show_plot_arcs_ui,
generate_batch_ui
)
from ui.setting_tab import build_setting_tab, load_novel_architecture, save_novel_architecture
from ui.directory_tab import build_directory_tab, load_chapter_blueprint, save_chapter_blueprint
from ui.character_tab import build_character_tab, load_character_state, save_character_state
from ui.summary_tab import build_summary_tab, load_global_summary, save_global_summary
from ui.chapters_tab import build_chapters_tab, refresh_chapters_list, on_chapter_selected, load_chapter_content, save_current_chapter, prev_chapter, next_chapter
from ui.other_settings import build_other_settings_tab
class NovelGeneratorGUI:
"""
小说生成器的主GUI类,包含所有的界面布局、事件处理、与后端逻辑的交互等。
"""
def __init__(self, master):
self.master = master
self.master.title("Novel Generator GUI")
try:
if os.path.exists("icon.ico"):
self.master.iconbitmap("icon.ico")
except Exception:
pass
self.master.geometry("1350x840")
# --------------- 配置文件路径 ---------------
self.config_file = "config.json"
self.loaded_config = load_config(self.config_file)
if self.loaded_config:
last_llm = next(iter(self.loaded_config["llm_configs"].values())).get("interface_format", "OpenAI")
last_embedding = self.loaded_config.get("last_embedding_interface_format", "OpenAI")
else:
last_llm = "OpenAI"
last_embedding = "OpenAI"
# if self.loaded_config and "llm_configs" in self.loaded_config and last_llm in self.loaded_config["llm_configs"]:
# llm_conf = next(iter(self.loaded_config["llm_configs"]))
# else:
# llm_conf = {
# "api_key": "",
# "base_url": "https://api.openai.com/v1",
# "model_name": "gpt-4o-mini",
# "temperature": 0.7,
# "max_tokens": 8192,
# "timeout": 600
# }
llm_conf = next(iter(self.loaded_config["llm_configs"].values()))
choose_configs = self.loaded_config.get("choose_configs", {})
if self.loaded_config and "embedding_configs" in self.loaded_config and last_embedding in self.loaded_config["embedding_configs"]:
emb_conf = self.loaded_config["embedding_configs"][last_embedding]
else:
emb_conf = {
"api_key": "",
"base_url": "https://api.openai.com/v1",
"model_name": "text-embedding-ada-002",
"retrieval_k": 4
}
# PenBo 增加代理功能支持
proxy_url = self.loaded_config["proxy_setting"]["proxy_url"]
proxy_port = self.loaded_config["proxy_setting"]["proxy_port"]
if self.loaded_config["proxy_setting"]["enabled"]:
os.environ['HTTP_PROXY'] = f"http://{proxy_url}:{proxy_port}"
os.environ['HTTPS_PROXY'] = f"http://{proxy_url}:{proxy_port}"
else:
os.environ.pop('HTTP_PROXY', None)
os.environ.pop('HTTPS_PROXY', None)
# -- LLM通用参数 --
# self.llm_conf_name = next(iter(self.loaded_config["llm_configs"]))
self.api_key_var = ctk.StringVar(value=llm_conf.get("api_key", ""))
self.base_url_var = ctk.StringVar(value=llm_conf.get("base_url", "https://api.openai.com/v1"))
self.interface_format_var = ctk.StringVar(value=llm_conf.get("interface_format", "OpenAI"))
self.model_name_var = ctk.StringVar(value=llm_conf.get("model_name", "gpt-4o-mini"))
self.temperature_var = ctk.DoubleVar(value=llm_conf.get("temperature", 0.7))
self.max_tokens_var = ctk.IntVar(value=llm_conf.get("max_tokens", 8192))
self.timeout_var = ctk.IntVar(value=llm_conf.get("timeout", 600))
self.interface_config_var = ctk.StringVar(value=next(iter(self.loaded_config["llm_configs"])))
# -- Embedding相关 --
self.embedding_interface_format_var = ctk.StringVar(value=last_embedding)
self.embedding_api_key_var = ctk.StringVar(value=emb_conf.get("api_key", ""))
self.embedding_url_var = ctk.StringVar(value=emb_conf.get("base_url", "https://api.openai.com/v1"))
self.embedding_model_name_var = ctk.StringVar(value=emb_conf.get("model_name", "text-embedding-ada-002"))
self.embedding_retrieval_k_var = ctk.StringVar(value=str(emb_conf.get("retrieval_k", 4)))
# -- 生成配置相关 --
self.architecture_llm_var = ctk.StringVar(value=choose_configs.get("architecture_llm", "DeepSeek"))
self.chapter_outline_llm_var = ctk.StringVar(value=choose_configs.get("chapter_outline_llm", "DeepSeek"))
self.final_chapter_llm_var = ctk.StringVar(value=choose_configs.get("final_chapter_llm", "DeepSeek"))
self.consistency_review_llm_var = ctk.StringVar(value=choose_configs.get("consistency_review_llm", "DeepSeek"))
self.prompt_draft_llm_var = ctk.StringVar(value=choose_configs.get("prompt_draft_llm", "DeepSeek"))
# -- 小说参数相关 --
if self.loaded_config and "other_params" in self.loaded_config:
op = self.loaded_config["other_params"]
self.topic_default = op.get("topic", "")
self.genre_var = ctk.StringVar(value=op.get("genre", "玄幻"))
self.num_chapters_var = ctk.StringVar(value=str(op.get("num_chapters", 10)))
self.word_number_var = ctk.StringVar(value=str(op.get("word_number", 3000)))
self.filepath_var = ctk.StringVar(value=op.get("filepath", ""))
self.chapter_num_var = ctk.StringVar(value=str(op.get("chapter_num", "1")))
self.characters_involved_var = ctk.StringVar(value=op.get("characters_involved", ""))
self.key_items_var = ctk.StringVar(value=op.get("key_items", ""))
self.scene_location_var = ctk.StringVar(value=op.get("scene_location", ""))
self.time_constraint_var = ctk.StringVar(value=op.get("time_constraint", ""))
self.user_guidance_default = op.get("user_guidance", "")
self.webdav_url_var = ctk.StringVar(value=op.get("webdav_url", ""))
self.webdav_username_var = ctk.StringVar(value=op.get("webdav_username", ""))
self.webdav_password_var = ctk.StringVar(value=op.get("webdav_password", ""))
else:
self.topic_default = ""
self.genre_var = ctk.StringVar(value="玄幻")
self.num_chapters_var = ctk.StringVar(value="10")
self.word_number_var = ctk.StringVar(value="3000")
self.filepath_var = ctk.StringVar(value="")
self.chapter_num_var = ctk.StringVar(value="1")
self.characters_involved_var = ctk.StringVar(value="")
self.key_items_var = ctk.StringVar(value="")
self.scene_location_var = ctk.StringVar(value="")
self.time_constraint_var = ctk.StringVar(value="")
self.user_guidance_default = ""
# --------------- 整体Tab布局 ---------------
self.tabview = ctk.CTkTabview(self.master)
self.tabview.pack(fill="both", expand=True)
# 创建各个标签页
build_main_tab(self)
build_config_tabview(self)
build_novel_params_area(self, start_row=1)
build_optional_buttons_area(self, start_row=2)
build_setting_tab(self)
build_directory_tab(self)
build_character_tab(self)
build_summary_tab(self)
build_chapters_tab(self)
build_other_settings_tab(self)
# ----------------- 通用辅助函数 -----------------
def show_tooltip(self, key: str):
info_text = tooltips.get(key, "暂无说明")
messagebox.showinfo("参数说明", info_text)
def safe_get_int(self, var, default=1):
try:
val_str = str(var.get()).strip()
return int(val_str)
except:
var.set(str(default))
return default
def log(self, message: str):
self.log_text.configure(state="normal")
self.log_text.insert("end", message + "\n")
self.log_text.see("end")
self.log_text.configure(state="disabled")
def safe_log(self, message: str):
self.master.after(0, lambda: self.log(message))
def disable_button_safe(self, btn):
self.master.after(0, lambda: btn.configure(state="disabled"))
def enable_button_safe(self, btn):
self.master.after(0, lambda: btn.configure(state="normal"))
def handle_exception(self, context: str):
full_message = f"{context}\n{traceback.format_exc()}"
logging.error(full_message)
self.safe_log(full_message)
def show_chapter_in_textbox(self, text: str):
self.chapter_result.delete("0.0", "end")
self.chapter_result.insert("0.0", text)
self.chapter_result.see("end")
def test_llm_config(self):
"""
测试当前的LLM配置是否可用
"""
interface_format = self.interface_format_var.get().strip()
api_key = self.api_key_var.get().strip()
base_url = self.base_url_var.get().strip()
model_name = self.model_name_var.get().strip()
temperature = self.temperature_var.get()
max_tokens = self.max_tokens_var.get()
timeout = self.timeout_var.get()
test_llm_config(
interface_format=interface_format,
api_key=api_key,
base_url=base_url,
model_name=model_name,
temperature=temperature,
max_tokens=max_tokens,
timeout=timeout,
log_func=self.safe_log,
handle_exception_func=self.handle_exception
)
def test_embedding_config(self):
"""
测试当前的Embedding配置是否可用
"""
api_key = self.embedding_api_key_var.get().strip()
base_url = self.embedding_url_var.get().strip()
interface_format = self.embedding_interface_format_var.get().strip()
model_name = self.embedding_model_name_var.get().strip()
test_embedding_config(
api_key=api_key,
base_url=base_url,
interface_format=interface_format,
model_name=model_name,
log_func=self.safe_log,
handle_exception_func=self.handle_exception
)
def browse_folder(self):
selected_dir = filedialog.askdirectory()
if selected_dir:
self.filepath_var.set(selected_dir)
def show_character_import_window(self):
"""显示角色导入窗口"""
import_window = ctk.CTkToplevel(self.master)
import_window.title("导入角色信息")
import_window.geometry("600x500")
import_window.transient(self.master) # 设置为父窗口的临时窗口
import_window.grab_set() # 保持窗口在顶层
# 主容器
main_frame = ctk.CTkFrame(import_window)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# 滚动容器
scroll_frame = ctk.CTkScrollableFrame(main_frame)
scroll_frame.pack(fill="both", expand=True, padx=5, pady=5)
# 获取角色库路径
role_lib_path = os.path.join(self.filepath_var.get().strip(), "角色库")
self.selected_roles = [] # 存储选中的角色名称
# 动态加载角色分类
if os.path.exists(role_lib_path):
# 配置网格布局参数
scroll_frame.columnconfigure(0, weight=1)
max_roles_per_row = 4
current_row = 0
for category in os.listdir(role_lib_path):
category_path = os.path.join(role_lib_path, category)
if os.path.isdir(category_path):
# 创建分类容器
category_frame = ctk.CTkFrame(scroll_frame)
category_frame.grid(row=current_row, column=0, sticky="w", pady=(10,5), padx=5)
# 添加分类标签
category_label = ctk.CTkLabel(category_frame, text=f"【{category}】",
font=("Microsoft YaHei", 12, "bold"))
category_label.grid(row=0, column=0, padx=(0,10), sticky="w")
# 初始化角色排列参数
role_count = 0
row_num = 0
col_num = 1 # 从第1列开始(第0列是分类标签)
# 添加角色复选框
for role_file in os.listdir(category_path):
if role_file.endswith(".txt"):
role_name = os.path.splitext(role_file)[0]
if not any(name == role_name for _, name in self.selected_roles):
chk = ctk.CTkCheckBox(category_frame, text=role_name)
chk.grid(row=row_num, column=col_num, padx=5, pady=2, sticky="w")
self.selected_roles.append((chk, role_name))
# 更新行列位置
role_count += 1
col_num += 1
if col_num > max_roles_per_row:
col_num = 1
row_num += 1
# 如果没有角色,调整分类标签占满整行
if role_count == 0:
category_label.grid(columnspan=max_roles_per_row+1, sticky="w")
# 更新主布局的行号
current_row += 1
# 添加分隔线
separator = ctk.CTkFrame(scroll_frame, height=1, fg_color="gray")
separator.grid(row=current_row, column=0, sticky="ew", pady=5)
current_row += 1
# 底部按钮框架
btn_frame = ctk.CTkFrame(main_frame)
btn_frame.pack(fill="x", pady=10)
# 选择按钮
def confirm_selection():
selected = [name for chk, name in self.selected_roles if chk.get() == 1]
self.char_inv_text.delete("0.0", "end")
self.char_inv_text.insert("0.0", ", ".join(selected))
import_window.destroy()
btn_confirm = ctk.CTkButton(btn_frame, text="选择", command=confirm_selection)
btn_confirm.pack(side="left", padx=20)
# 取消按钮
btn_cancel = ctk.CTkButton(btn_frame, text="取消", command=import_window.destroy)
btn_cancel.pack(side="right", padx=20)
def show_role_library(self):
save_path = self.filepath_var.get().strip()
if not save_path:
messagebox.showwarning("警告", "请先设置保存路径")
return
# 初始化LLM适配器
llm_adapter = create_llm_adapter(
interface_format=self.interface_format_var.get(),
base_url=self.base_url_var.get(),
model_name=self.model_name_var.get(),
api_key=self.api_key_var.get(),
temperature=self.temperature_var.get(),
max_tokens=self.max_tokens_var.get(),
timeout=self.timeout_var.get()
)
# 传递LLM适配器实例到角色库
if hasattr(self, '_role_lib'):
if self._role_lib.window and self._role_lib.window.winfo_exists():
self._role_lib.window.destroy()
self._role_lib = RoleLibrary(self.master, save_path, llm_adapter) # 新增参数
# ----------------- 将导入的各模块函数直接赋给类方法 -----------------
generate_novel_architecture_ui = generate_novel_architecture_ui
generate_chapter_blueprint_ui = generate_chapter_blueprint_ui
generate_chapter_draft_ui = generate_chapter_draft_ui
finalize_chapter_ui = finalize_chapter_ui
do_consistency_check = do_consistency_check
generate_batch_ui = generate_batch_ui
import_knowledge_handler = import_knowledge_handler
clear_vectorstore_handler = clear_vectorstore_handler
show_plot_arcs_ui = show_plot_arcs_ui
load_config_btn = load_config_btn
save_config_btn = save_config_btn
load_novel_architecture = load_novel_architecture
save_novel_architecture = save_novel_architecture
load_chapter_blueprint = load_chapter_blueprint
save_chapter_blueprint = save_chapter_blueprint
load_character_state = load_character_state
save_character_state = save_character_state
load_global_summary = load_global_summary
save_global_summary = save_global_summary
refresh_chapters_list = refresh_chapters_list
on_chapter_selected = on_chapter_selected
save_current_chapter = save_current_chapter
prev_chapter = prev_chapter
next_chapter = next_chapter
test_llm_config = test_llm_config
test_embedding_config = test_embedding_config
browse_folder = browse_folder