Merge pull request #136 from CNlaojing/main

增加角色库功能
This commit is contained in:
IdleCloud
2025-03-15 17:23:35 +08:00
committed by GitHub
18 changed files with 2130 additions and 138 deletions
+9
View File
@@ -18,6 +18,15 @@
> 一款基于大语言模型的多功能小说生成器,助您高效创作逻辑严谨、设定统一的长篇故事 > 一款基于大语言模型的多功能小说生成器,助您高效创作逻辑严谨、设定统一的长篇故事
2025-03-05 添加角色库功能
2025-03-09 添加字数显示
2025-03-13
1、新增闲云修改;
2、把本章指导改成内容指导;
3、在生成架构中的: 2. 角色动力学设定(角色弧光模型)、 3. 世界构建矩阵(三维度交织法)、 4. 情节架构(三幕式悬念)与生成目录的:5. 章节目录生成(悬念节奏曲线)加入引导词内容指导,以方便生成角色动力学时只以核心种子生成,导致生成的内容与实际需求不符。
4、在终端加回被删除的LLM提示词与LLM返回内容显示,以便复盘,参考修改提示词。
--- ---
## 📑 目录导航 ## 📑 目录导航
-2
View File
@@ -3,11 +3,9 @@
import logging import logging
import traceback import traceback
from typing import List from typing import List
import requests import requests
from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings
def ensure_openai_base_url_has_v1(url: str) -> str: def ensure_openai_base_url_has_v1(url: str) -> str:
""" """
若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1' 若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'
+49 -6
View File
@@ -9,6 +9,7 @@ from azure.ai.inference import ChatCompletionsClient
from azure.core.credentials import AzureKeyCredential from azure.core.credentials import AzureKeyCredential
from azure.ai.inference.models import SystemMessage, UserMessage from azure.ai.inference.models import SystemMessage, UserMessage
from openai import OpenAI from openai import OpenAI
import requests
def check_base_url(url: str) -> str: def check_base_url(url: str) -> str:
@@ -114,7 +115,8 @@ class GeminiAdapter(BaseLLMAdapter):
config = types.GenerateContentConfig( config = types.GenerateContentConfig(
max_output_tokens=self.max_tokens, max_output_tokens=self.max_tokens,
temperature=self.temperature, temperature=self.temperature,
) ),
timeout=self.timeout # 添加超时参数
) )
if response and response.text: if response and response.text:
return response.text return response.text
@@ -212,11 +214,15 @@ class MLStudioAdapter(BaseLLMAdapter):
) )
def invoke(self, prompt: str) -> str: def invoke(self, prompt: str) -> str:
try:
response = self._client.invoke(prompt) response = self._client.invoke(prompt)
if not response: if not response:
logging.warning("No response from MLStudioAdapter.") logging.warning("No response from MLStudioAdapter.")
return "" return ""
return response.content return response.content
except Exception as e:
logging.error(f"ML Studio API 调用超时或失败: {e}")
return ""
class AzureAIAdapter(BaseLLMAdapter): class AzureAIAdapter(BaseLLMAdapter):
""" """
@@ -279,24 +285,59 @@ class VolcanoEngineAIAdapter(BaseLLMAdapter):
self.timeout = timeout self.timeout = timeout
self._client = OpenAI( self._client = OpenAI(
# 此为默认路径,您可根据业务所在地域进行配置
base_url=base_url, base_url=base_url,
# 从环境变量中获取您的 API Key api_key=api_key,
api_key=api_key timeout=timeout # 添加超时配置
) )
def invoke(self, prompt: str) -> str: def invoke(self, prompt: str) -> str:
try:
response = self._client.chat.completions.create( response = self._client.chat.completions.create(
model=self.model_name, # bot-20250223190248-2bq5k 为您当前的智能体的ID,注意此处与Chat API存在差异。差异对比详见 SDK使用指南 model=self.model_name,
messages=[ messages=[
{"role": "system", "content": "你是DeepSeek,是一个 AI 人工智能助手"}, {"role": "system", "content": "你是DeepSeek,是一个 AI 人工智能助手"},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
timeout=self.timeout # 添加超时参数
) )
# response = self._client.invoke(prompt)
if not response: if not response:
logging.warning("No response from DeepSeekAdapter.") logging.warning("No response from DeepSeekAdapter.")
return "" return ""
return response.choices[0].message.content return response.choices[0].message.content
except Exception as e:
logging.error(f"火山引擎API调用超时或失败: {e}")
return ""
class SiliconFlowAdapter(BaseLLMAdapter):
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600):
self.base_url = check_base_url(base_url)
self.api_key = api_key
self.model_name = model_name
self.max_tokens = max_tokens
self.temperature = temperature
self.timeout = timeout
self._client = OpenAI(
base_url=base_url,
api_key=api_key,
timeout=timeout # 添加超时配置
)
def invoke(self, prompt: str) -> str:
try:
response = self._client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": "你是DeepSeek,是一个 AI 人工智能助手"},
{"role": "user", "content": prompt},
],
timeout=self.timeout # 添加超时参数
)
if not response:
logging.warning("No response from DeepSeekAdapter.")
return ""
return response.choices[0].message.content
except Exception as e:
logging.error(f"硅基流动API调用超时或失败: {e}")
return ""
def create_llm_adapter( def create_llm_adapter(
interface_format: str, interface_format: str,
@@ -330,5 +371,7 @@ def create_llm_adapter(
return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
elif fmt == "火山引擎": elif fmt == "火山引擎":
return VolcanoEngineAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) return VolcanoEngineAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
elif fmt == "硅基流动":
return SiliconFlowAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
else: else:
raise ValueError(f"Unknown interface_format: {interface_format}") raise ValueError(f"Unknown interface_format: {interface_format}")
+13 -4
View File
@@ -55,6 +55,7 @@ def Novel_architecture_generate(
number_of_chapters: int, number_of_chapters: int,
word_number: int, word_number: int,
filepath: str, filepath: str,
user_guidance: str = "", # 新增参数
temperature: float = 0.7, temperature: float = 0.7,
max_tokens: int = 2048, max_tokens: int = 2048,
timeout: int = 600 timeout: int = 600
@@ -91,7 +92,8 @@ def Novel_architecture_generate(
topic=topic, topic=topic,
genre=genre, genre=genre,
number_of_chapters=number_of_chapters, number_of_chapters=number_of_chapters,
word_number=word_number word_number=word_number,
user_guidance=user_guidance # 修复:添加内容指导
) )
core_seed_result = invoke_with_cleaning(llm_adapter, prompt_core) core_seed_result = invoke_with_cleaning(llm_adapter, prompt_core)
if not core_seed_result.strip(): if not core_seed_result.strip():
@@ -105,7 +107,10 @@ def Novel_architecture_generate(
# Step2: 角色动力学 # Step2: 角色动力学
if "character_dynamics_result" not in partial_data: if "character_dynamics_result" not in partial_data:
logging.info("Step2: Generating character_dynamics_prompt ...") logging.info("Step2: Generating character_dynamics_prompt ...")
prompt_character = character_dynamics_prompt.format(core_seed=partial_data["core_seed_result"].strip()) prompt_character = character_dynamics_prompt.format(
core_seed=partial_data["core_seed_result"].strip(),
user_guidance=user_guidance
)
character_dynamics_result = invoke_with_cleaning(llm_adapter, prompt_character) character_dynamics_result = invoke_with_cleaning(llm_adapter, prompt_character)
if not character_dynamics_result.strip(): if not character_dynamics_result.strip():
logging.warning("character_dynamics_prompt generation failed.") logging.warning("character_dynamics_prompt generation failed.")
@@ -135,7 +140,10 @@ def Novel_architecture_generate(
# Step3: 世界观 # Step3: 世界观
if "world_building_result" not in partial_data: if "world_building_result" not in partial_data:
logging.info("Step3: Generating world_building_prompt ...") logging.info("Step3: Generating world_building_prompt ...")
prompt_world = world_building_prompt.format(core_seed=partial_data["core_seed_result"].strip()) prompt_world = world_building_prompt.format(
core_seed=partial_data["core_seed_result"].strip(),
user_guidance=user_guidance # 修复:添加用户指导
)
world_building_result = invoke_with_cleaning(llm_adapter, prompt_world) world_building_result = invoke_with_cleaning(llm_adapter, prompt_world)
if not world_building_result.strip(): if not world_building_result.strip():
logging.warning("world_building_prompt generation failed.") logging.warning("world_building_prompt generation failed.")
@@ -151,7 +159,8 @@ def Novel_architecture_generate(
prompt_plot = plot_architecture_prompt.format( prompt_plot = plot_architecture_prompt.format(
core_seed=partial_data["core_seed_result"].strip(), core_seed=partial_data["core_seed_result"].strip(),
character_dynamics=partial_data["character_dynamics_result"].strip(), character_dynamics=partial_data["character_dynamics_result"].strip(),
world_building=partial_data["world_building_result"].strip() world_building=partial_data["world_building_result"].strip(),
user_guidance=user_guidance # 修复:添加用户指导
) )
plot_arch_result = invoke_with_cleaning(llm_adapter, prompt_plot) plot_arch_result = invoke_with_cleaning(llm_adapter, prompt_plot)
if not plot_arch_result.strip(): if not plot_arch_result.strip():
+26 -10
View File
@@ -5,7 +5,6 @@
""" """
import os import os
import logging import logging
from nltk import download
from llm_adapters import create_llm_adapter from llm_adapters import create_llm_adapter
from prompt_definitions import first_chapter_draft_prompt, next_chapter_draft_prompt, summarize_recent_chapters_prompt from prompt_definitions import first_chapter_draft_prompt, next_chapter_draft_prompt, summarize_recent_chapters_prompt
from chapter_directory_parser import get_chapter_info_from_blueprint from chapter_directory_parser import get_chapter_info_from_blueprint
@@ -91,7 +90,7 @@ def build_chapter_prompt(
timeout: int = 600 timeout: int = 600
) -> str: ) -> str:
""" """
构造当前章节的请求提示词,不调用 LLM,仅返回构造好的提示词字符串。 构造当前章节的请求提示词,新增对下一章节元数据的引用
""" """
arch_file = os.path.join(filepath, "Novel_architecture.txt") arch_file = os.path.join(filepath, "Novel_architecture.txt")
novel_architecture_text = read_file(arch_file) novel_architecture_text = read_file(arch_file)
@@ -102,6 +101,7 @@ def build_chapter_prompt(
character_state_file = os.path.join(filepath, "character_state.txt") character_state_file = os.path.join(filepath, "character_state.txt")
character_state_text = read_file(character_state_file) character_state_text = read_file(character_state_file)
# 获取当前章节信息
chapter_info = get_chapter_info_from_blueprint(blueprint_text, novel_number) chapter_info = get_chapter_info_from_blueprint(blueprint_text, novel_number)
chapter_title = chapter_info["chapter_title"] chapter_title = chapter_info["chapter_title"]
chapter_role = chapter_info["chapter_role"] chapter_role = chapter_info["chapter_role"]
@@ -111,6 +111,17 @@ def build_chapter_prompt(
plot_twist_level = chapter_info["plot_twist_level"] plot_twist_level = chapter_info["plot_twist_level"]
chapter_summary = chapter_info["chapter_summary"] chapter_summary = chapter_info["chapter_summary"]
# 新增:获取下一章节信息
next_chapter_number = novel_number + 1
next_chapter_info = get_chapter_info_from_blueprint(blueprint_text, next_chapter_number)
next_chapter_title = next_chapter_info.get("chapter_title", "(未命名)")
next_chapter_role = next_chapter_info.get("chapter_role", "过渡章节")
next_chapter_purpose = next_chapter_info.get("chapter_purpose", "承上启下")
next_chapter_suspense = next_chapter_info.get("suspense_level", "中等")
next_chapter_foreshadow = next_chapter_info.get("foreshadowing", "无特殊伏笔")
next_chapter_twist = next_chapter_info.get("plot_twist_level", "★☆☆☆☆")
next_chapter_summary = next_chapter_info.get("chapter_summary", "衔接过渡内容")
chapters_dir = os.path.join(filepath, "chapters") chapters_dir = os.path.join(filepath, "chapters")
os.makedirs(chapters_dir, exist_ok=True) os.makedirs(chapters_dir, exist_ok=True)
@@ -187,7 +198,17 @@ def build_chapter_prompt(
global_summary=global_summary_text, global_summary=global_summary_text,
character_state=character_state_text, character_state=character_state_text,
context_excerpt=relevant_context, context_excerpt=relevant_context,
previous_chapter_excerpt=previous_chapter_excerpt previous_chapter_excerpt=previous_chapter_excerpt,
# 新增下一章节参数
next_chapter_number=next_chapter_number,
next_chapter_title=next_chapter_title,
next_chapter_role=next_chapter_role,
next_chapter_purpose=next_chapter_purpose,
next_chapter_suspense_level=next_chapter_suspense,
next_chapter_foreshadowing=next_chapter_foreshadow,
next_chapter_plot_twist_level=next_chapter_twist,
next_chapter_summary=next_chapter_summary
) )
return prompt_text return prompt_text
@@ -212,16 +233,11 @@ def generate_chapter_draft(
interface_format: str = "openai", interface_format: str = "openai",
max_tokens: int = 2048, max_tokens: int = 2048,
timeout: int = 600, timeout: int = 600,
custom_prompt_text: str = None # 新增参数,若不为 None,则使用用户编辑后的提示词 custom_prompt_text: str = None
) -> str: ) -> str:
""" """
根据 novel_number 判断是否为第一章。 生成章节草稿,支持自定义提示词
- 若是第一章,则使用 first_chapter_draft_prompt
- 否则使用 next_chapter_draft_prompt
若 custom_prompt_text 提供,则以此作为提示词进行生成。
最终将生成文本存入 chapters/chapter_{novel_number}.txt。
""" """
# 构造提示词:若用户提供了编辑后的提示词,则使用之;否则构造默认提示词
if custom_prompt_text is None: if custom_prompt_text is None:
prompt_text = build_chapter_prompt( prompt_text = build_chapter_prompt(
api_key=api_key, api_key=api_key,
+32 -14
View File
@@ -42,18 +42,36 @@ def debug_log(prompt: str, response_content: str):
f"\n[######################################### Response #########################################]\n{response_content}\n" f"\n[######################################### Response #########################################]\n{response_content}\n"
) )
def invoke_with_cleaning(llm_adapter, prompt: str) -> str: def invoke_with_cleaning(llm_adapter, prompt: str, max_retries: int = 3) -> str:
""" """调用 LLM 并清理返回结果"""
调用 LLM,增加重试和清洗逻辑 print("\n" + "="*50)
如果多次失败,则返回空字符串以继续流程,而不是中断。 print("发送到 LLM 的提示词:")
""" print("-"*50)
def _invoke(prompt): print(prompt)
return llm_adapter.invoke(prompt) print("="*50 + "\n")
result = ""
retry_count = 0
while retry_count < max_retries:
try:
result = llm_adapter.invoke(prompt)
print("\n" + "="*50)
print("LLM 返回的内容:")
print("-"*50)
print(result)
print("="*50 + "\n")
# 清理结果中的特殊格式标记
result = result.replace("```", "").strip()
if result:
return result
retry_count += 1
except Exception as e:
print(f"调用失败 ({retry_count + 1}/{max_retries}): {str(e)}")
retry_count += 1
if retry_count >= max_retries:
raise e
return result
response = call_with_retry(func=_invoke, max_retries=3, fallback_return="", prompt=prompt)
if not response:
logging.warning("No response from model after retry. Return empty.")
return ""
cleaned_text = remove_think_tags(response)
debug_log(prompt, cleaned_text)
return cleaned_text.strip()
+129 -58
View File
@@ -40,8 +40,9 @@ core_seed_prompt = """\
# =============== 2. 角色动力学设定(角色弧光模型)=================== # =============== 2. 角色动力学设定(角色弧光模型)===================
character_dynamics_prompt = """\ character_dynamics_prompt = """\
基于核心种子 基于以下元素
{core_seed} - 内容指导:{user_guidance}
- 核心种子:{core_seed}
请设计3-6个具有动态变化潜力的核心角色,每个角色需包含: 请设计3-6个具有动态变化潜力的核心角色,每个角色需包含:
特征: 特征:
@@ -68,7 +69,11 @@ character_dynamics_prompt = """\
# =============== 3. 世界构建矩阵(三维度交织法)=================== # =============== 3. 世界构建矩阵(三维度交织法)===================
world_building_prompt = """\ world_building_prompt = """\
为服务核心冲突"{core_seed}",请构建三维交织的世界观 基于以下元素
- 内容指导:{user_guidance}
- 核心冲突:"{core_seed}"
为服务上述内容,请构建三维交织的世界观:
1. 物理维度: 1. 物理维度:
- 空间结构(地理×社会阶层分布图) - 空间结构(地理×社会阶层分布图)
@@ -92,10 +97,11 @@ world_building_prompt = """\
# =============== 4. 情节架构(三幕式悬念)=================== # =============== 4. 情节架构(三幕式悬念)===================
plot_architecture_prompt = """\ plot_architecture_prompt = """\
基于以下元素构建三幕式悬念架构 基于以下元素:
核心种子:{core_seed} - 内容指导:{user_guidance}
角色体系:{character_dynamics} - 核心种子:{core_seed}
世界观:{world_building} - 角色体系:{character_dynamics}
- 世界观:{world_building}
要求按以下结构设计: 要求按以下结构设计:
第一幕(触发) 第一幕(触发)
@@ -121,7 +127,9 @@ plot_architecture_prompt = """\
# =============== 5. 章节目录生成(悬念节奏曲线)=================== # =============== 5. 章节目录生成(悬念节奏曲线)===================
chapter_blueprint_prompt = """\ chapter_blueprint_prompt = """\
根据小说架构:\n 基于以下元素:
- 内容指导:{user_guidance}
- 小说架构:
{novel_architecture} {novel_architecture}
设计{number_of_chapters}章的节奏分布: 设计{number_of_chapters}章的节奏分布:
@@ -163,7 +171,9 @@ chapter_blueprint_prompt = """\
""" """
chunked_chapter_blueprint_prompt = """\ chunked_chapter_blueprint_prompt = """\
根据小说架构:\n 基于以下元素:
- 内容指导:{user_guidance}
- 小说架构:
{novel_architecture} {novel_architecture}
需要生成总共{number_of_chapters}章的节奏分布, 需要生成总共{number_of_chapters}章的节奏分布,
@@ -232,40 +242,45 @@ create_character_state_prompt = """\
依据当前角色动力学设定:{character_dynamics} 依据当前角色动力学设定:{character_dynamics}
请生成一个角色状态文档,内容格式: 请生成一个角色状态文档,内容格式:
角色A属性
李员外:
├──物品: ├──物品:
├──物品(若有初始物品则增加,没有则为暂无):描述 ├──青衫:一件破损的青色长袍,带有暗红色的污渍
... │ └──寒铁长剑:一柄断裂的铁剑,剑身上刻有古老的符文
├──能力 ├──能力
├──技能1(若有初始技能则增加,没有则为暂无):描述 ├──技能1:强大的精神感知能力:能够察觉到周围人的心中活动
... │ └──技能2:无形攻击:能够释放一种无法被视觉捕捉的精神攻击
├──状态 ├──状态
├──身体状态 ├──身体状态: 身材挺拔,穿着华丽的铠甲,面色冷峻
──Buff/Debuff ──心理状态: 目前的心态比较平静,但内心隐藏着对柳溪镇未来掌控的野心和不安
├──心理状态:描述
├──主要角色间关系网 ├──主要角色间关系网
├──角色B:描述(初始有关联则增加,没有则为暂无关系) ├──林婉儿:李员外从小就与她有关联,对她的成长一直保持关注
├──角色C:描述(初始有关联则增加,没有则为暂无关系) │ └──苏明远:两人之间有着复杂的过去,最近因一场冲突而让对方感到威胁
...
├──触发或加深的事件 ├──触发或加深的事件
├──暂无事件 ├──村庄内突然出现不明符号:这个不明符号似乎在暗示柳溪镇即将发生重大事件
... │ └──林婉儿被刺穿皮肤:这次事件让两人意识到对方的强大实力,促使他们迅速离开队伍
角色B属性 角色
├──物品 ├──物品:
├──... ├──某物(道具):描述
│ └──XX长剑(武器):描述
│ ...
├──能力 ├──能力
├──... ├──技能1:描述
│ └──技能2:描述
│ ...
├──状态 ├──状态
├──... ├──身体状态:
│ └──心理状态:描述
├──主要角色间关系网 ├──主要角色间关系网
├──... ├──角色B:描述
│ └──角色C:描述
│ ...
├──触发或加深的事件 ├──触发或加深的事件
├──... ├──事件1:描述
│ └──事件2:描述
角色C属性: ...
......
新出场角色: 新出场角色:
- (此处填写未来任何新增角色或临时出场人物的基本信息) - (此处填写未来任何新增角色或临时出场人物的基本信息)
@@ -282,42 +297,46 @@ update_character_state_prompt = """\
{old_state} {old_state}
请更新主要角色状态,内容格式: 请更新主要角色状态,内容格式:
角色A属性
李员外:
├──物品: ├──物品:
├──某物(道具):描述 ├──青衫:一件破损的青色长袍,带有暗红色的污渍
├──XX长剑(武器):描述 │ └──寒铁长剑:一柄断裂的铁剑,剑身上刻有古老的符文
...
├──能力 ├──能力
├──技能1描述 ├──技能1强大的精神感知能力:能够察觉到周围人的心中活动
──技能2描述 ──技能2无形攻击:能够释放一种无法被视觉捕捉的精神攻击
...
├──状态 ├──状态
├──身体状态 ├──身体状态: 身材挺拔,穿着华丽的铠甲,面色冷峻
──Buff/Debuff ──心理状态: 目前的心态比较平静,但内心隐藏着对柳溪镇未来掌控的野心和不安
├──心理状态:描述
├──主要角色间关系网 ├──主要角色间关系网
├──角色B:描述 ├──林婉儿:李员外从小就与她有关联,对她的成长一直保持关注
├──角色C:描述 │ └──苏明远:两人之间有着复杂的过去,最近因一场冲突而让对方感到威胁
...
├──触发或加深的事件 ├──触发或加深的事件
├──事件1:描述 ├──村庄内突然出现不明符号:这个不明符号似乎在暗示柳溪镇即将发生重大事件
──事件2:描述 ──林婉儿被刺穿皮肤:这次事件让两人意识到对方的强大实力,促使他们迅速离开队伍
...
角色B属性 角色
├──物品 ├──物品:
├──... ├──某物(道具):描述
│ └──XX长剑(武器):描述
│ ...
├──能力 ├──能力
├──... ├──技能1:描述
│ └──技能2:描述
│ ...
├──状态 ├──状态
├──... ├──身体状态:
│ └──心理状态:描述
├──主要角色间关系网 ├──主要角色间关系网
├──... ├──角色B:描述
│ └──角色C:描述
│ ...
├──触发或加深的事件 ├──触发或加深的事件
├──... ├──事件1:描述
│ └──事件2:描述
...
角色C属性:
...... ......
新出场角色: 新出场角色:
@@ -407,6 +426,16 @@ next_chapter_draft_prompt = """\
认知颠覆:{plot_twist_level} 认知颠覆:{plot_twist_level}
本章简述:{chapter_summary} 本章简述:{chapter_summary}
下一章节介绍:
{next_chapter_number} 章《{next_chapter_title}
本章定位:{next_chapter_role}
核心作用:{next_chapter_purpose}
悬念密度:{next_chapter_suspense_level}
伏笔操作:{next_chapter_foreshadowing}
认知颠覆:{next_chapter_plot_twist_level}
本章简述:{next_chapter_summary}
参考下一章内容简介,避免情节脱节或冲突。
可用元素: 可用元素:
- 核心人物(可能未指定){characters_involved} - 核心人物(可能未指定){characters_involved}
- 关键道具(可能未指定){key_items} - 关键道具(可能未指定){key_items}
@@ -449,3 +478,45 @@ next_chapter_draft_prompt = """\
额外指导(可能未指定){user_guidance} 额外指导(可能未指定){user_guidance}
""" """
Character_Import_Prompt = """\
根据以下文本内容,分析出所有角色及其属性信息,严格按照以下格式要求:
<<角色状态格式要求>>
1. 必须包含以下五个分类(按顺序):
● 物品 ● 能力 ● 状态 ● 主要角色间关系网 ● 触发或加深的事件
2. 每个属性条目必须用【名称: 描述】格式
例:├──青衫: 一件破损的青色长袍,带有暗红色的污渍
3. 状态必须包含:
● 身体状态: [当前身体状况]
● 心理状态: [当前心理状况]
4. 关系网格式:
● [角色名称]: [关系类型,如"竞争对手"/"盟友"]
5. 触发事件格式:
● [事件名称]: [简要描述及影响]
<<示例>>
李员外:
├──物品:
│ ├──青衫: 一件破损的青色长袍,带有暗红色污渍
│ └──寒铁长剑: 剑身有裂痕,刻有「青云」符文
├──能力:
│ ├──精神感知: 能感知半径30米内的生命体
│ └──剑气压制: 通过目光释放精神威压
├──状态:
│ ├──身体状态: 右臂有未愈合的刀伤
│ └──心理状态: 对苏明远的实力感到忌惮
├──主要角色间关系网:
│ ├──苏明远: 竞争对手,十年前的同僚
│ └──林婉儿: 暗中培养的继承人
├──触发或加深的事件:
│ ├──兵器库遇袭: 丢失三把传家宝剑,影响战力
│ └──匿名威胁信: 信纸带有檀香味,暗示内部泄密
请严格按上述格式分析以下内容:
<<待分析小说文本开始>>
{content}
<<待分析小说文本结束>>
"""
+13 -2
View File
@@ -34,11 +34,22 @@ def build_chapters_tab(self):
save_btn.grid(row=0, column=3, padx=5, pady=5, sticky="w") save_btn.grid(row=0, column=3, padx=5, pady=5, sticky="w")
refresh_btn = ctk.CTkButton(top_frame, text="刷新章节列表", command=self.refresh_chapters_list, font=("Microsoft YaHei", 12)) refresh_btn = ctk.CTkButton(top_frame, text="刷新章节列表", command=self.refresh_chapters_list, font=("Microsoft YaHei", 12))
refresh_btn.grid(row=0, column=4, padx=5, pady=5, sticky="e") refresh_btn.grid(row=0, column=5, padx=5, pady=5, sticky="e")
self.chapters_word_count_label = ctk.CTkLabel(top_frame, text="字数:0", font=("Microsoft YaHei", 12))
self.chapters_word_count_label.grid(row=0, column=4, padx=(0,10), sticky="e")
self.chapter_view_text = ctk.CTkTextbox(self.chapters_view_tab, wrap="word", font=("Microsoft YaHei", 12)) self.chapter_view_text = ctk.CTkTextbox(self.chapters_view_tab, wrap="word", font=("Microsoft YaHei", 12))
def update_word_count(event=None):
text = self.chapter_view_text.get("0.0", "end-1c")
text_length = len(text)
self.chapters_word_count_label.configure(text=f"字数:{text_length}")
self.chapter_view_text.bind("<KeyRelease>", update_word_count)
self.chapter_view_text.bind("<ButtonRelease>", update_word_count)
TextWidgetContextMenu(self.chapter_view_text) TextWidgetContextMenu(self.chapter_view_text)
self.chapter_view_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) self.chapter_view_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5, columnspan=6)
self.chapters_list = [] self.chapters_list = []
refresh_chapters_list(self) refresh_chapters_list(self)
+13 -2
View File
@@ -15,12 +15,23 @@ def build_character_tab(self):
load_btn = ctk.CTkButton(self.character_tab, text="加载 character_state.txt", command=self.load_character_state, font=("Microsoft YaHei", 12)) load_btn = ctk.CTkButton(self.character_tab, text="加载 character_state.txt", command=self.load_character_state, font=("Microsoft YaHei", 12))
load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.character_wordcount_label = ctk.CTkLabel(self.character_tab, text="字数:0", font=("Microsoft YaHei", 12))
self.character_wordcount_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
save_btn = ctk.CTkButton(self.character_tab, text="保存修改", command=self.save_character_state, font=("Microsoft YaHei", 12)) save_btn = ctk.CTkButton(self.character_tab, text="保存修改", command=self.save_character_state, font=("Microsoft YaHei", 12))
save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") save_btn.grid(row=0, column=2, padx=5, pady=5, sticky="e")
self.character_text = ctk.CTkTextbox(self.character_tab, wrap="word", font=("Microsoft YaHei", 12)) self.character_text = ctk.CTkTextbox(self.character_tab, wrap="word", font=("Microsoft YaHei", 12))
def update_word_count(event=None):
text = self.character_text.get("0.0", "end-1c")
text_length = len(text)
self.character_wordcount_label.configure(text=f"字数:{text_length}")
self.character_text.bind("<KeyRelease>", update_word_count)
self.character_text.bind("<ButtonRelease>", update_word_count)
TextWidgetContextMenu(self.character_text) TextWidgetContextMenu(self.character_text)
self.character_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) self.character_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5, columnspan=3)
def load_character_state(self): def load_character_state(self):
filepath = self.filepath_var.get().strip() filepath = self.filepath_var.get().strip()
+7 -1
View File
@@ -92,6 +92,9 @@ def build_ai_config_tab(self):
elif new_value == "阿里云百炼": elif new_value == "阿里云百炼":
self.base_url_var.set("https://dashscope.aliyuncs.com/compatible-mode/v1") self.base_url_var.set("https://dashscope.aliyuncs.com/compatible-mode/v1")
self.model_name_var.set("qwen-plus") self.model_name_var.set("qwen-plus")
elif new_value == "硅基流动":
self.base_url_var.set("https://api.siliconflow.cn/v1")
self.model_name_var.set("deepseek-ai/DeepSeek-V3")
for i in range(7): for i in range(7):
self.ai_config_tab.grid_rowconfigure(i, weight=0) self.ai_config_tab.grid_rowconfigure(i, weight=0)
@@ -111,7 +114,8 @@ def build_ai_config_tab(self):
# 3) 接口格式 # 3) 接口格式
create_label_with_help(self, parent=self.ai_config_tab, label_text="LLM 接口格式:", tooltip_key="interface_format", row=2, column=0, font=("Microsoft YaHei", 12)) create_label_with_help(self, parent=self.ai_config_tab, label_text="LLM 接口格式:", tooltip_key="interface_format", row=2, column=0, font=("Microsoft YaHei", 12))
interface_options = ["DeepSeek", "阿里云百炼", "OpenAI", "Azure OpenAI", "Azure AI", "Ollama", "ML Studio", "Gemini","火山引擎"] # 在这里的接口选项列表中添加 "硅基流动"
interface_options = ["DeepSeek", "阿里云百炼", "OpenAI", "Azure OpenAI", "Azure AI", "Ollama", "ML Studio", "Gemini", "火山引擎", "硅基流动"]
interface_dropdown = ctk.CTkOptionMenu(self.ai_config_tab, values=interface_options, variable=self.interface_format_var, command=on_interface_format_changed, font=("Microsoft YaHei", 12)) interface_dropdown = ctk.CTkOptionMenu(self.ai_config_tab, values=interface_options, variable=self.interface_format_var, command=on_interface_format_changed, font=("Microsoft YaHei", 12))
interface_dropdown.grid(row=2, column=1, padx=5, pady=5, columnspan=2, sticky="nsew") interface_dropdown.grid(row=2, column=1, padx=5, pady=5, columnspan=2, sticky="nsew")
@@ -197,7 +201,9 @@ def build_embeddings_config_tab(self):
# 2) Embedding 接口格式 # 2) Embedding 接口格式
create_label_with_help(self, parent=self.embeddings_config_tab, label_text="Embedding 接口格式:", tooltip_key="embedding_interface_format", row=1, column=0, font=("Microsoft YaHei", 12)) create_label_with_help(self, parent=self.embeddings_config_tab, label_text="Embedding 接口格式:", tooltip_key="embedding_interface_format", row=1, column=0, font=("Microsoft YaHei", 12))
emb_interface_options = ["DeepSeek", "OpenAI", "Azure OpenAI", "Gemini", "Ollama", "ML Studio","SiliconFlow"] emb_interface_options = ["DeepSeek", "OpenAI", "Azure OpenAI", "Gemini", "Ollama", "ML Studio","SiliconFlow"]
emb_interface_dropdown = ctk.CTkOptionMenu(self.embeddings_config_tab, values=emb_interface_options, variable=self.embedding_interface_format_var, command=on_embedding_interface_changed, font=("Microsoft YaHei", 12)) emb_interface_dropdown = ctk.CTkOptionMenu(self.embeddings_config_tab, values=emb_interface_options, variable=self.embedding_interface_format_var, command=on_embedding_interface_changed, font=("Microsoft YaHei", 12))
emb_interface_dropdown.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") emb_interface_dropdown.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
+13 -2
View File
@@ -15,12 +15,23 @@ def build_directory_tab(self):
load_btn = ctk.CTkButton(self.directory_tab, text="加载 Novel_directory.txt", command=self.load_chapter_blueprint, font=("Microsoft YaHei", 12)) load_btn = ctk.CTkButton(self.directory_tab, text="加载 Novel_directory.txt", command=self.load_chapter_blueprint, font=("Microsoft YaHei", 12))
load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.directory_word_count_label = ctk.CTkLabel(self.directory_tab, text="字数:0", font=("Microsoft YaHei", 12))
self.directory_word_count_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
save_btn = ctk.CTkButton(self.directory_tab, text="保存修改", command=self.save_chapter_blueprint, font=("Microsoft YaHei", 12)) save_btn = ctk.CTkButton(self.directory_tab, text="保存修改", command=self.save_chapter_blueprint, font=("Microsoft YaHei", 12))
save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") save_btn.grid(row=0, column=2, padx=5, pady=5, sticky="e")
self.directory_text = ctk.CTkTextbox(self.directory_tab, wrap="word", font=("Microsoft YaHei", 12)) self.directory_text = ctk.CTkTextbox(self.directory_tab, wrap="word", font=("Microsoft YaHei", 12))
def update_word_count(event=None):
text = self.directory_text.get("0.0", "end")
count = len(text) - 1
self.directory_word_count_label.configure(text=f"字数:{count}")
self.directory_text.bind("<KeyRelease>", update_word_count)
self.directory_text.bind("<ButtonRelease>", update_word_count)
TextWidgetContextMenu(self.directory_text) TextWidgetContextMenu(self.directory_text)
self.directory_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) self.directory_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5, columnspan=3)
def load_chapter_blueprint(self): def load_chapter_blueprint(self):
filepath = self.filepath_var.get().strip() filepath = self.filepath_var.get().strip()
+80 -3
View File
@@ -25,6 +25,11 @@ def generate_novel_architecture_ui(self):
return return
def task(): def task():
confirm = messagebox.askyesno("确认", "确定要生成小说架构吗?")
if not confirm:
self.enable_button_safe(self.btn_generate_architecture)
return
self.disable_button_safe(self.btn_generate_architecture) self.disable_button_safe(self.btn_generate_architecture)
try: try:
interface_format = self.interface_format_var.get().strip() interface_format = self.interface_format_var.get().strip()
@@ -39,6 +44,8 @@ def generate_novel_architecture_ui(self):
genre = self.genre_var.get().strip() genre = self.genre_var.get().strip()
num_chapters = self.safe_get_int(self.num_chapters_var, 10) num_chapters = self.safe_get_int(self.num_chapters_var, 10)
word_number = self.safe_get_int(self.word_number_var, 3000) word_number = self.safe_get_int(self.word_number_var, 3000)
# 获取内容指导
user_guidance = self.user_guide_text.get("0.0", "end").strip()
self.safe_log("开始生成小说架构...") self.safe_log("开始生成小说架构...")
Novel_architecture_generate( Novel_architecture_generate(
@@ -53,7 +60,8 @@ def generate_novel_architecture_ui(self):
filepath=filepath, filepath=filepath,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
timeout=timeout_val timeout=timeout_val,
user_guidance=user_guidance # 添加内容指导参数
) )
self.safe_log("✅ 小说架构生成完成。请在 'Novel Architecture' 标签页查看或编辑。") self.safe_log("✅ 小说架构生成完成。请在 'Novel Architecture' 标签页查看或编辑。")
except Exception: except Exception:
@@ -69,6 +77,9 @@ def generate_chapter_blueprint_ui(self):
return return
def task(): def task():
if not messagebox.askyesno("确认", "确定要生成章节草稿吗?"):
self.enable_button_safe(self.btn_generate_chapter)
return
self.disable_button_safe(self.btn_generate_directory) self.disable_button_safe(self.btn_generate_directory)
try: try:
interface_format = self.interface_format_var.get().strip() interface_format = self.interface_format_var.get().strip()
@@ -168,7 +179,64 @@ def generate_chapter_draft_ui(self):
dialog.geometry("600x400") dialog.geometry("600x400")
text_box = ctk.CTkTextbox(dialog, wrap="word", font=("Microsoft YaHei", 12)) text_box = ctk.CTkTextbox(dialog, wrap="word", font=("Microsoft YaHei", 12))
text_box.pack(fill="both", expand=True, padx=10, pady=10) text_box.pack(fill="both", expand=True, padx=10, pady=10)
text_box.insert("0.0", prompt_text)
# 字数统计标签
wordcount_label = ctk.CTkLabel(dialog, text="字数:0", font=("Microsoft YaHei", 12))
wordcount_label.pack(side="left", padx=(10,0), pady=5)
# 插入角色内容
final_prompt = prompt_text
role_names = [name.strip() for name in self.char_inv_text.get("0.0", "end").strip().split(',') if name.strip()]
role_lib_path = os.path.join(filepath, "角色库")
role_contents = []
if os.path.exists(role_lib_path):
for root, dirs, files in os.walk(role_lib_path):
for file in files:
if file.endswith(".txt") and os.path.splitext(file)[0] in role_names:
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
role_contents.append(f.read().strip()) # 直接使用文件内容,不添加重复名字
except Exception as e:
self.safe_log(f"读取角色文件 {file} 失败: {str(e)}")
if role_contents:
role_content_str = "\n".join(role_contents)
# 更精确的替换逻辑,处理不同情况下的占位符
placeholder_variations = [
"核心人物(可能未指定){characters_involved}",
"核心人物:{characters_involved}",
"核心人物(可能未指定):{characters_involved}",
"核心人物:{characters_involved}"
]
for placeholder in placeholder_variations:
if placeholder in final_prompt:
final_prompt = final_prompt.replace(
placeholder,
f"核心人物:\n{role_content_str}"
)
break
else: # 如果没有找到任何已知占位符变体
lines = final_prompt.split('\n')
for i, line in enumerate(lines):
if "核心人物" in line and "" in line:
lines[i] = f"核心人物:\n{role_content_str}"
break
final_prompt = '\n'.join(lines)
text_box.insert("0.0", final_prompt)
# 更新字数函数
def update_word_count(event=None):
text = text_box.get("0.0", "end-1c")
text_length = len(text)
wordcount_label.configure(text=f"字数:{text_length}")
text_box.bind("<KeyRelease>", update_word_count)
text_box.bind("<ButtonRelease>", update_word_count)
update_word_count() # 初始化统计
button_frame = ctk.CTkFrame(dialog) button_frame = ctk.CTkFrame(dialog)
button_frame.pack(pady=10) button_frame.pack(pady=10)
def on_confirm(): def on_confirm():
@@ -236,6 +304,10 @@ def finalize_chapter_ui(self):
return return
def task(): def task():
if not messagebox.askyesno("确认", "确定要定稿当前章节吗?"):
self.enable_button_safe(self.btn_finalize_chapter)
return
self.disable_button_safe(self.btn_finalize_chapter) self.disable_button_safe(self.btn_finalize_chapter)
try: try:
interface_format = self.interface_format_var.get().strip() interface_format = self.interface_format_var.get().strip()
@@ -385,7 +457,12 @@ def import_knowledge_handler(self):
self.handle_exception("导入知识库时出错") self.handle_exception("导入知识库时出错")
finally: finally:
self.enable_button_safe(self.btn_import_knowledge) self.enable_button_safe(self.btn_import_knowledge)
threading.Thread(target=task, daemon=True).start() try:
thread = threading.Thread(target=task, daemon=True)
thread.start()
except Exception as e:
self.enable_button_safe(self.btn_import_knowledge)
messagebox.showerror("错误", f"线程启动失败: {str(e)}")
def clear_vectorstore_handler(self): def clear_vectorstore_handler(self):
filepath = self.filepath_var.get().strip() filepath = self.filepath_var.get().strip()
+12 -2
View File
@@ -33,14 +33,24 @@ def build_left_layout(self):
self.left_frame.grid_rowconfigure(4, weight=1) self.left_frame.grid_rowconfigure(4, weight=1)
self.left_frame.columnconfigure(0, weight=1) self.left_frame.columnconfigure(0, weight=1)
chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容 (可编辑)", font=("Microsoft YaHei", 12)) self.chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容可编辑 字数:0", font=("Microsoft YaHei", 12))
chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w") self.chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
# 章节文本编辑框 # 章节文本编辑框
self.chapter_result = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 14)) self.chapter_result = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 14))
TextWidgetContextMenu(self.chapter_result) TextWidgetContextMenu(self.chapter_result)
self.chapter_result.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) self.chapter_result.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
def update_word_count(event=None):
text = self.chapter_result.get("0.0", "end")
count = len(text) - 1 # 减去最后一个换行符
self.chapter_label.configure(text=f"本章内容(可编辑) 字数:{count}")
self.chapter_result.bind("<KeyRelease>", update_word_count)
self.chapter_result.bind("<ButtonRelease>", update_word_count)
# Step 按钮区域 # Step 按钮区域
self.step_buttons_frame = ctk.CTkFrame(self.left_frame) self.step_buttons_frame = ctk.CTkFrame(self.left_frame)
self.step_buttons_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5) self.step_buttons_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
+116
View File
@@ -7,6 +7,8 @@ import traceback
import customtkinter as ctk import customtkinter as ctk
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox 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 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 utils import read_file, save_string_to_txt, clear_file_content
@@ -222,6 +224,120 @@ class NovelGeneratorGUI:
if selected_dir: if selected_dir:
self.filepath_var.set(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_novel_architecture_ui = generate_novel_architecture_ui
generate_chapter_blueprint_ui = generate_chapter_blueprint_ui generate_chapter_blueprint_ui = generate_chapter_blueprint_ui
+44 -9
View File
@@ -55,9 +55,9 @@ def build_novel_params_area(self, start_row=1):
chapter_num_entry = ctk.CTkEntry(self.params_frame, textvariable=self.chapter_num_var, width=80, font=("Microsoft YaHei", 12)) chapter_num_entry = ctk.CTkEntry(self.params_frame, textvariable=self.chapter_num_var, width=80, font=("Microsoft YaHei", 12))
chapter_num_entry.grid(row=row_chap_num, column=1, padx=5, pady=5, sticky="w") chapter_num_entry.grid(row=row_chap_num, column=1, padx=5, pady=5, sticky="w")
# 6) 本章指导 # 6) 内容指导
row_user_guide = 5 row_user_guide = 5
create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="本章指导:", tooltip_key="user_guidance", row=row_user_guide, column=0, font=("Microsoft YaHei", 12), sticky="ne") create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="内容指导:", tooltip_key="user_guidance", row=row_user_guide, column=0, font=("Microsoft YaHei", 12), sticky="ne")
self.user_guide_text = ctk.CTkTextbox(self.params_frame, height=80, wrap="word", font=("Microsoft YaHei", 12)) self.user_guide_text = ctk.CTkTextbox(self.params_frame, height=80, wrap="word", font=("Microsoft YaHei", 12))
TextWidgetContextMenu(self.user_guide_text) TextWidgetContextMenu(self.user_guide_text)
self.user_guide_text.grid(row=row_user_guide, column=1, padx=5, pady=5, sticky="nsew") self.user_guide_text.grid(row=row_user_guide, column=1, padx=5, pady=5, sticky="nsew")
@@ -67,8 +67,24 @@ def build_novel_params_area(self, start_row=1):
# 7) 可选元素:核心人物/关键道具/空间坐标/时间压力 # 7) 可选元素:核心人物/关键道具/空间坐标/时间压力
row_idx = 6 row_idx = 6
create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="核心人物:", tooltip_key="characters_involved", row=row_idx, column=0, font=("Microsoft YaHei", 12)) create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="核心人物:", tooltip_key="characters_involved", row=row_idx, column=0, font=("Microsoft YaHei", 12))
char_inv_entry = ctk.CTkEntry(self.params_frame, textvariable=self.characters_involved_var, font=("Microsoft YaHei", 12))
char_inv_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") # 核心人物输入框+按钮容器
char_inv_frame = ctk.CTkFrame(self.params_frame)
char_inv_frame.grid(row=row_idx, column=1, padx=5, pady=5, sticky="nsew")
char_inv_frame.columnconfigure(0, weight=1)
char_inv_frame.rowconfigure(0, weight=1)
# 三行文本输入框
self.char_inv_text = ctk.CTkTextbox(char_inv_frame, height=60, wrap="word", font=("Microsoft YaHei", 12))
self.char_inv_text.grid(row=0, column=0, padx=(0,5), pady=5, sticky="nsew")
if hasattr(self, 'characters_involved_var'):
self.char_inv_text.insert("0.0", self.characters_involved_var.get())
# 导入按钮
import_btn = ctk.CTkButton(char_inv_frame, text="导入", width=60,
command=self.show_character_import_window,
font=("Microsoft YaHei", 12))
import_btn.grid(row=0, column=1, padx=(0,5), pady=5, sticky="e")
row_idx += 1 row_idx += 1
create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="关键道具:", tooltip_key="key_items", row=row_idx, column=0, font=("Microsoft YaHei", 12)) create_label_with_help_for_novel_params(self, parent=self.params_frame, label_text="关键道具:", tooltip_key="key_items", row=row_idx, column=0, font=("Microsoft YaHei", 12))
key_items_entry = ctk.CTkEntry(self.params_frame, textvariable=self.key_items_var, font=("Microsoft YaHei", 12)) key_items_entry = ctk.CTkEntry(self.params_frame, textvariable=self.key_items_var, font=("Microsoft YaHei", 12))
@@ -85,20 +101,39 @@ def build_novel_params_area(self, start_row=1):
def build_optional_buttons_area(self, start_row=2): def build_optional_buttons_area(self, start_row=2):
self.optional_btn_frame = ctk.CTkFrame(self.right_frame) self.optional_btn_frame = ctk.CTkFrame(self.right_frame)
self.optional_btn_frame.grid(row=start_row, column=0, sticky="ew", padx=5, pady=5) self.optional_btn_frame.grid(row=start_row, column=0, sticky="ew", padx=5, pady=5)
self.optional_btn_frame.columnconfigure((0, 1, 2, 3), weight=1) self.optional_btn_frame.columnconfigure((0, 1, 2, 3, 4), weight=1)
self.btn_check_consistency = ctk.CTkButton(self.optional_btn_frame, text="一致性审校", command=self.do_consistency_check, font=("Microsoft YaHei", 12)) self.btn_check_consistency = ctk.CTkButton(
self.optional_btn_frame, text="一致性审校", command=self.do_consistency_check,
font=("Microsoft YaHei", 12), width=100 # 固定宽度
)
self.btn_check_consistency.grid(row=0, column=0, padx=5, pady=5, sticky="ew") self.btn_check_consistency.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
self.btn_import_knowledge = ctk.CTkButton(self.optional_btn_frame, text="导入知识库", command=self.import_knowledge_handler, font=("Microsoft YaHei", 12)) self.btn_import_knowledge = ctk.CTkButton(
self.optional_btn_frame, text="导入知识库", command=self.import_knowledge_handler,
font=("Microsoft YaHei", 12), width=100
)
self.btn_import_knowledge.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.btn_import_knowledge.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.btn_clear_vectorstore = ctk.CTkButton(self.optional_btn_frame, text="清空向量库", fg_color="red", command=self.clear_vectorstore_handler, font=("Microsoft YaHei", 12)) self.btn_clear_vectorstore = ctk.CTkButton(
self.optional_btn_frame, text="清空向量库", fg_color="red",
command=self.clear_vectorstore_handler, font=("Microsoft YaHei", 12), width=100
)
self.btn_clear_vectorstore.grid(row=0, column=2, padx=5, pady=5, sticky="ew") self.btn_clear_vectorstore.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
self.plot_arcs_btn = ctk.CTkButton(self.optional_btn_frame, text="查看剧情要点", command=self.show_plot_arcs_ui, font=("Microsoft YaHei", 12)) self.plot_arcs_btn = ctk.CTkButton(
self.optional_btn_frame, text="查看剧情要点", command=self.show_plot_arcs_ui,
font=("Microsoft YaHei", 12), width=100
)
self.plot_arcs_btn.grid(row=0, column=3, padx=5, pady=5, sticky="ew") self.plot_arcs_btn.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
# 新增角色库按钮
self.role_library_btn = ctk.CTkButton(
self.optional_btn_frame, text="角色库", command=self.show_role_library,
font=("Microsoft YaHei", 12), width=100
)
self.role_library_btn.grid(row=0, column=4, padx=5, pady=5, sticky="ew")
def create_label_with_help_for_novel_params(self, parent, label_text, tooltip_key, row, column, font=None, sticky="e", padx=5, pady=5): def create_label_with_help_for_novel_params(self, parent, label_text, tooltip_key, row, column, font=None, sticky="e", padx=5, pady=5):
frame = ctk.CTkFrame(parent) frame = ctk.CTkFrame(parent)
frame.grid(row=row, column=column, padx=padx, pady=pady, sticky=sticky) frame.grid(row=row, column=column, padx=padx, pady=pady, sticky=sticky)
+1528
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -15,12 +15,23 @@ def build_setting_tab(self):
load_btn = ctk.CTkButton(self.setting_tab, text="加载 Novel_architecture.txt", command=self.load_novel_architecture, font=("Microsoft YaHei", 12)) load_btn = ctk.CTkButton(self.setting_tab, text="加载 Novel_architecture.txt", command=self.load_novel_architecture, font=("Microsoft YaHei", 12))
load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.setting_word_count_label = ctk.CTkLabel(self.setting_tab, text="字数:0", font=("Microsoft YaHei", 12))
self.setting_word_count_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
save_btn = ctk.CTkButton(self.setting_tab, text="保存修改", command=self.save_novel_architecture, font=("Microsoft YaHei", 12)) save_btn = ctk.CTkButton(self.setting_tab, text="保存修改", command=self.save_novel_architecture, font=("Microsoft YaHei", 12))
save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") save_btn.grid(row=0, column=2, padx=5, pady=5, sticky="e")
self.setting_text = ctk.CTkTextbox(self.setting_tab, wrap="word", font=("Microsoft YaHei", 12)) self.setting_text = ctk.CTkTextbox(self.setting_tab, wrap="word", font=("Microsoft YaHei", 12))
TextWidgetContextMenu(self.setting_text) TextWidgetContextMenu(self.setting_text)
self.setting_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) self.setting_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5, columnspan=3)
def update_word_count(event=None):
text = self.setting_text.get("0.0", "end")
count = len(text) - 1
self.setting_word_count_label.configure(text=f"字数:{count}")
self.setting_text.bind("<KeyRelease>", update_word_count)
self.setting_text.bind("<ButtonRelease>", update_word_count)
def load_novel_architecture(self): def load_novel_architecture(self):
filepath = self.filepath_var.get().strip() filepath = self.filepath_var.get().strip()
+14 -2
View File
@@ -11,17 +11,29 @@ def build_summary_tab(self):
self.summary_tab.rowconfigure(0, weight=0) self.summary_tab.rowconfigure(0, weight=0)
self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.rowconfigure(1, weight=1)
self.summary_tab.columnconfigure(0, weight=1) self.summary_tab.columnconfigure(0, weight=1)
self.summary_tab.columnconfigure(1, weight=0)
self.summary_tab.columnconfigure(2, weight=0)
load_btn = ctk.CTkButton(self.summary_tab, text="加载 global_summary.txt", command=self.load_global_summary, font=("Microsoft YaHei", 12)) load_btn = ctk.CTkButton(self.summary_tab, text="加载 global_summary.txt", command=self.load_global_summary, font=("Microsoft YaHei", 12))
load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.word_count_label = ctk.CTkLabel(self.summary_tab, text="字数:0", font=("Microsoft YaHei", 12))
self.word_count_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
save_btn = ctk.CTkButton(self.summary_tab, text="保存修改", command=self.save_global_summary, font=("Microsoft YaHei", 12)) save_btn = ctk.CTkButton(self.summary_tab, text="保存修改", command=self.save_global_summary, font=("Microsoft YaHei", 12))
save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") save_btn.grid(row=0, column=2, padx=5, pady=5, sticky="e")
self.summary_text = ctk.CTkTextbox(self.summary_tab, wrap="word", font=("Microsoft YaHei", 12)) self.summary_text = ctk.CTkTextbox(self.summary_tab, wrap="word", font=("Microsoft YaHei", 12))
TextWidgetContextMenu(self.summary_text) TextWidgetContextMenu(self.summary_text)
self.summary_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) self.summary_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5, columnspan=3)
def update_word_count(event=None):
text = self.summary_text.get("0.0", "end")
count = len(text) - 1
self.word_count_label.configure(text=f"字数:{count}")
self.summary_text.bind("<KeyRelease>", update_word_count)
self.summary_text.bind("<ButtonRelease>", update_word_count)
def load_global_summary(self): def load_global_summary(self):
filepath = self.filepath_var.get().strip() filepath = self.filepath_var.get().strip()
if not filepath: if not filepath: