@@ -18,6 +18,15 @@
|
||||
|
||||
> 一款基于大语言模型的多功能小说生成器,助您高效创作逻辑严谨、设定统一的长篇故事
|
||||
|
||||
2025-03-05 添加角色库功能
|
||||
|
||||
2025-03-09 添加字数显示
|
||||
|
||||
2025-03-13
|
||||
1、新增闲云修改;
|
||||
2、把本章指导改成内容指导;
|
||||
3、在生成架构中的: 2. 角色动力学设定(角色弧光模型)、 3. 世界构建矩阵(三维度交织法)、 4. 情节架构(三幕式悬念)与生成目录的:5. 章节目录生成(悬念节奏曲线)加入引导词内容指导,以方便生成角色动力学时只以核心种子生成,导致生成的内容与实际需求不符。
|
||||
4、在终端加回被删除的LLM提示词与LLM返回内容显示,以便复盘,参考修改提示词。
|
||||
---
|
||||
|
||||
## 📑 目录导航
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
import logging
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings
|
||||
|
||||
|
||||
def ensure_openai_base_url_has_v1(url: str) -> str:
|
||||
"""
|
||||
若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'。
|
||||
|
||||
+49
-6
@@ -9,6 +9,7 @@ from azure.ai.inference import ChatCompletionsClient
|
||||
from azure.core.credentials import AzureKeyCredential
|
||||
from azure.ai.inference.models import SystemMessage, UserMessage
|
||||
from openai import OpenAI
|
||||
import requests
|
||||
|
||||
|
||||
def check_base_url(url: str) -> str:
|
||||
@@ -114,7 +115,8 @@ class GeminiAdapter(BaseLLMAdapter):
|
||||
config = types.GenerateContentConfig(
|
||||
max_output_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
)
|
||||
),
|
||||
timeout=self.timeout # 添加超时参数
|
||||
)
|
||||
if response and response.text:
|
||||
return response.text
|
||||
@@ -212,11 +214,15 @@ class MLStudioAdapter(BaseLLMAdapter):
|
||||
)
|
||||
|
||||
def invoke(self, prompt: str) -> str:
|
||||
try:
|
||||
response = self._client.invoke(prompt)
|
||||
if not response:
|
||||
logging.warning("No response from MLStudioAdapter.")
|
||||
return ""
|
||||
return response.content
|
||||
except Exception as e:
|
||||
logging.error(f"ML Studio API 调用超时或失败: {e}")
|
||||
return ""
|
||||
|
||||
class AzureAIAdapter(BaseLLMAdapter):
|
||||
"""
|
||||
@@ -279,24 +285,59 @@ class VolcanoEngineAIAdapter(BaseLLMAdapter):
|
||||
self.timeout = timeout
|
||||
|
||||
self._client = OpenAI(
|
||||
# 此为默认路径,您可根据业务所在地域进行配置
|
||||
base_url=base_url,
|
||||
# 从环境变量中获取您的 API Key
|
||||
api_key=api_key
|
||||
api_key=api_key,
|
||||
timeout=timeout # 添加超时配置
|
||||
)
|
||||
def invoke(self, prompt: str) -> str:
|
||||
try:
|
||||
response = self._client.chat.completions.create(
|
||||
model=self.model_name, # bot-20250223190248-2bq5k 为您当前的智能体的ID,注意此处与Chat API存在差异。差异对比详见 SDK使用指南
|
||||
model=self.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是DeepSeek,是一个 AI 人工智能助手"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
timeout=self.timeout # 添加超时参数
|
||||
)
|
||||
# response = self._client.invoke(prompt)
|
||||
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 ""
|
||||
|
||||
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(
|
||||
interface_format: str,
|
||||
@@ -330,5 +371,7 @@ def create_llm_adapter(
|
||||
return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
|
||||
elif fmt == "火山引擎":
|
||||
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:
|
||||
raise ValueError(f"Unknown interface_format: {interface_format}")
|
||||
|
||||
@@ -55,6 +55,7 @@ def Novel_architecture_generate(
|
||||
number_of_chapters: int,
|
||||
word_number: int,
|
||||
filepath: str,
|
||||
user_guidance: str = "", # 新增参数
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2048,
|
||||
timeout: int = 600
|
||||
@@ -91,7 +92,8 @@ def Novel_architecture_generate(
|
||||
topic=topic,
|
||||
genre=genre,
|
||||
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)
|
||||
if not core_seed_result.strip():
|
||||
@@ -105,7 +107,10 @@ def Novel_architecture_generate(
|
||||
# Step2: 角色动力学
|
||||
if "character_dynamics_result" not in partial_data:
|
||||
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)
|
||||
if not character_dynamics_result.strip():
|
||||
logging.warning("character_dynamics_prompt generation failed.")
|
||||
@@ -135,7 +140,10 @@ def Novel_architecture_generate(
|
||||
# Step3: 世界观
|
||||
if "world_building_result" not in partial_data:
|
||||
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)
|
||||
if not world_building_result.strip():
|
||||
logging.warning("world_building_prompt generation failed.")
|
||||
@@ -151,7 +159,8 @@ def Novel_architecture_generate(
|
||||
prompt_plot = plot_architecture_prompt.format(
|
||||
core_seed=partial_data["core_seed_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)
|
||||
if not plot_arch_result.strip():
|
||||
|
||||
+26
-10
@@ -5,7 +5,6 @@
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from nltk import download
|
||||
from llm_adapters import create_llm_adapter
|
||||
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
|
||||
@@ -91,7 +90,7 @@ def build_chapter_prompt(
|
||||
timeout: int = 600
|
||||
) -> str:
|
||||
"""
|
||||
构造当前章节的请求提示词,不调用 LLM,仅返回构造好的提示词字符串。
|
||||
构造当前章节的请求提示词,新增对下一章节元数据的引用
|
||||
"""
|
||||
arch_file = os.path.join(filepath, "Novel_architecture.txt")
|
||||
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_text = read_file(character_state_file)
|
||||
|
||||
# 获取当前章节信息
|
||||
chapter_info = get_chapter_info_from_blueprint(blueprint_text, novel_number)
|
||||
chapter_title = chapter_info["chapter_title"]
|
||||
chapter_role = chapter_info["chapter_role"]
|
||||
@@ -111,6 +111,17 @@ def build_chapter_prompt(
|
||||
plot_twist_level = chapter_info["plot_twist_level"]
|
||||
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")
|
||||
os.makedirs(chapters_dir, exist_ok=True)
|
||||
|
||||
@@ -187,7 +198,17 @@ def build_chapter_prompt(
|
||||
global_summary=global_summary_text,
|
||||
character_state=character_state_text,
|
||||
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
|
||||
|
||||
@@ -212,16 +233,11 @@ def generate_chapter_draft(
|
||||
interface_format: str = "openai",
|
||||
max_tokens: int = 2048,
|
||||
timeout: int = 600,
|
||||
custom_prompt_text: str = None # 新增参数,若不为 None,则使用用户编辑后的提示词
|
||||
custom_prompt_text: str = None
|
||||
) -> 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:
|
||||
prompt_text = build_chapter_prompt(
|
||||
api_key=api_key,
|
||||
|
||||
+32
-14
@@ -42,18 +42,36 @@ def debug_log(prompt: str, response_content: str):
|
||||
f"\n[######################################### Response #########################################]\n{response_content}\n"
|
||||
)
|
||||
|
||||
def invoke_with_cleaning(llm_adapter, prompt: str) -> str:
|
||||
"""
|
||||
调用 LLM,增加重试和清洗逻辑
|
||||
如果多次失败,则返回空字符串以继续流程,而不是中断。
|
||||
"""
|
||||
def _invoke(prompt):
|
||||
return llm_adapter.invoke(prompt)
|
||||
def invoke_with_cleaning(llm_adapter, prompt: str, max_retries: int = 3) -> str:
|
||||
"""调用 LLM 并清理返回结果"""
|
||||
print("\n" + "="*50)
|
||||
print("发送到 LLM 的提示词:")
|
||||
print("-"*50)
|
||||
print(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
@@ -40,8 +40,9 @@ core_seed_prompt = """\
|
||||
|
||||
# =============== 2. 角色动力学设定(角色弧光模型)===================
|
||||
character_dynamics_prompt = """\
|
||||
基于核心种子:
|
||||
{core_seed}
|
||||
基于以下元素:
|
||||
- 内容指导:{user_guidance}
|
||||
- 核心种子:{core_seed}
|
||||
|
||||
请设计3-6个具有动态变化潜力的核心角色,每个角色需包含:
|
||||
特征:
|
||||
@@ -68,7 +69,11 @@ character_dynamics_prompt = """\
|
||||
|
||||
# =============== 3. 世界构建矩阵(三维度交织法)===================
|
||||
world_building_prompt = """\
|
||||
为服务核心冲突"{core_seed}",请构建三维交织的世界观:
|
||||
基于以下元素:
|
||||
- 内容指导:{user_guidance}
|
||||
- 核心冲突:"{core_seed}"
|
||||
|
||||
为服务上述内容,请构建三维交织的世界观:
|
||||
|
||||
1. 物理维度:
|
||||
- 空间结构(地理×社会阶层分布图)
|
||||
@@ -92,10 +97,11 @@ world_building_prompt = """\
|
||||
|
||||
# =============== 4. 情节架构(三幕式悬念)===================
|
||||
plot_architecture_prompt = """\
|
||||
基于以下元素构建三幕式悬念架构:
|
||||
核心种子:{core_seed}
|
||||
角色体系:{character_dynamics}
|
||||
世界观:{world_building}
|
||||
基于以下元素:
|
||||
- 内容指导:{user_guidance}
|
||||
- 核心种子:{core_seed}
|
||||
- 角色体系:{character_dynamics}
|
||||
- 世界观:{world_building}
|
||||
|
||||
要求按以下结构设计:
|
||||
第一幕(触发)
|
||||
@@ -121,7 +127,9 @@ plot_architecture_prompt = """\
|
||||
|
||||
# =============== 5. 章节目录生成(悬念节奏曲线)===================
|
||||
chapter_blueprint_prompt = """\
|
||||
根据小说架构:\n
|
||||
基于以下元素:
|
||||
- 内容指导:{user_guidance}
|
||||
- 小说架构:
|
||||
{novel_architecture}
|
||||
|
||||
设计{number_of_chapters}章的节奏分布:
|
||||
@@ -163,7 +171,9 @@ chapter_blueprint_prompt = """\
|
||||
"""
|
||||
|
||||
chunked_chapter_blueprint_prompt = """\
|
||||
根据小说架构:\n
|
||||
基于以下元素:
|
||||
- 内容指导:{user_guidance}
|
||||
- 小说架构:
|
||||
{novel_architecture}
|
||||
|
||||
需要生成总共{number_of_chapters}章的节奏分布,
|
||||
@@ -232,40 +242,45 @@ create_character_state_prompt = """\
|
||||
依据当前角色动力学设定:{character_dynamics}
|
||||
|
||||
请生成一个角色状态文档,内容格式:
|
||||
角色A属性:
|
||||
例:
|
||||
李员外:
|
||||
├──物品:
|
||||
├──物品(若有初始物品则增加,没有则为暂无):描述
|
||||
...
|
||||
│ ├──青衫:一件破损的青色长袍,带有暗红色的污渍
|
||||
│ └──寒铁长剑:一柄断裂的铁剑,剑身上刻有古老的符文
|
||||
├──能力
|
||||
├──技能1(若有初始技能则增加,没有则为暂无):描述
|
||||
...
|
||||
│ ├──技能1:强大的精神感知能力:能够察觉到周围人的心中活动
|
||||
│ └──技能2:无形攻击:能够释放一种无法被视觉捕捉的精神攻击
|
||||
├──状态
|
||||
├──身体状态:
|
||||
├──Buff/Debuff
|
||||
├──心理状态:描述
|
||||
|
||||
│ ├──身体状态: 身材挺拔,穿着华丽的铠甲,面色冷峻
|
||||
│ └──心理状态: 目前的心态比较平静,但内心隐藏着对柳溪镇未来掌控的野心和不安
|
||||
├──主要角色间关系网
|
||||
├──角色B:描述(初始有关联则增加,没有则为暂无关系)
|
||||
├──角色C:描述(初始有关联则增加,没有则为暂无关系)
|
||||
...
|
||||
│ ├──林婉儿:李员外从小就与她有关联,对她的成长一直保持关注
|
||||
│ └──苏明远:两人之间有着复杂的过去,最近因一场冲突而让对方感到威胁
|
||||
├──触发或加深的事件
|
||||
├──暂无事件
|
||||
...
|
||||
│ ├──村庄内突然出现不明符号:这个不明符号似乎在暗示柳溪镇即将发生重大事件
|
||||
│ └──林婉儿被刺穿皮肤:这次事件让两人意识到对方的强大实力,促使他们迅速离开队伍
|
||||
|
||||
角色B属性:
|
||||
├──物品
|
||||
├──...
|
||||
角色名:
|
||||
├──物品:
|
||||
│ ├──某物(道具):描述
|
||||
│ └──XX长剑(武器):描述
|
||||
│ ...
|
||||
├──能力
|
||||
├──...
|
||||
│ ├──技能1:描述
|
||||
│ └──技能2:描述
|
||||
│ ...
|
||||
├──状态
|
||||
├──...
|
||||
│ ├──身体状态:
|
||||
│ └──心理状态:描述
|
||||
│
|
||||
├──主要角色间关系网
|
||||
├──...
|
||||
│ ├──角色B:描述
|
||||
│ └──角色C:描述
|
||||
│ ...
|
||||
├──触发或加深的事件
|
||||
├──...
|
||||
|
||||
角色C属性:
|
||||
......
|
||||
│ ├──事件1:描述
|
||||
│ └──事件2:描述
|
||||
...
|
||||
|
||||
新出场角色:
|
||||
- (此处填写未来任何新增角色或临时出场人物的基本信息)
|
||||
@@ -282,42 +297,46 @@ update_character_state_prompt = """\
|
||||
{old_state}
|
||||
|
||||
请更新主要角色状态,内容格式:
|
||||
角色A属性:
|
||||
例:
|
||||
李员外:
|
||||
├──物品:
|
||||
├──某物(道具):描述
|
||||
├──XX长剑(武器):描述
|
||||
...
|
||||
│ ├──青衫:一件破损的青色长袍,带有暗红色的污渍
|
||||
│ └──寒铁长剑:一柄断裂的铁剑,剑身上刻有古老的符文
|
||||
├──能力
|
||||
├──技能1:描述
|
||||
├──技能2:描述
|
||||
...
|
||||
│ ├──技能1:强大的精神感知能力:能够察觉到周围人的心中活动
|
||||
│ └──技能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}
|
||||
本章简述:{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}
|
||||
- 关键道具(可能未指定):{key_items}
|
||||
@@ -449,3 +478,45 @@ next_chapter_draft_prompt = """\
|
||||
|
||||
额外指导(可能未指定):{user_guidance}
|
||||
"""
|
||||
|
||||
|
||||
Character_Import_Prompt = """\
|
||||
根据以下文本内容,分析出所有角色及其属性信息,严格按照以下格式要求:
|
||||
|
||||
<<角色状态格式要求>>
|
||||
1. 必须包含以下五个分类(按顺序):
|
||||
● 物品 ● 能力 ● 状态 ● 主要角色间关系网 ● 触发或加深的事件
|
||||
2. 每个属性条目必须用【名称: 描述】格式
|
||||
例:├──青衫: 一件破损的青色长袍,带有暗红色的污渍
|
||||
3. 状态必须包含:
|
||||
● 身体状态: [当前身体状况]
|
||||
● 心理状态: [当前心理状况]
|
||||
4. 关系网格式:
|
||||
● [角色名称]: [关系类型,如"竞争对手"/"盟友"]
|
||||
5. 触发事件格式:
|
||||
● [事件名称]: [简要描述及影响]
|
||||
|
||||
<<示例>>
|
||||
李员外:
|
||||
├──物品:
|
||||
│ ├──青衫: 一件破损的青色长袍,带有暗红色污渍
|
||||
│ └──寒铁长剑: 剑身有裂痕,刻有「青云」符文
|
||||
├──能力:
|
||||
│ ├──精神感知: 能感知半径30米内的生命体
|
||||
│ └──剑气压制: 通过目光释放精神威压
|
||||
├──状态:
|
||||
│ ├──身体状态: 右臂有未愈合的刀伤
|
||||
│ └──心理状态: 对苏明远的实力感到忌惮
|
||||
├──主要角色间关系网:
|
||||
│ ├──苏明远: 竞争对手,十年前的同僚
|
||||
│ └──林婉儿: 暗中培养的继承人
|
||||
├──触发或加深的事件:
|
||||
│ ├──兵器库遇袭: 丢失三把传家宝剑,影响战力
|
||||
│ └──匿名威胁信: 信纸带有檀香味,暗示内部泄密
|
||||
│
|
||||
|
||||
请严格按上述格式分析以下内容:
|
||||
<<待分析小说文本开始>>
|
||||
{content}
|
||||
<<待分析小说文本结束>>
|
||||
"""
|
||||
|
||||
+13
-2
@@ -34,11 +34,22 @@ def build_chapters_tab(self):
|
||||
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.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))
|
||||
|
||||
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)
|
||||
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 = []
|
||||
refresh_chapters_list(self)
|
||||
|
||||
+13
-2
@@ -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.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.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))
|
||||
|
||||
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)
|
||||
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):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
|
||||
+7
-1
@@ -92,6 +92,9 @@ def build_ai_config_tab(self):
|
||||
elif new_value == "阿里云百炼":
|
||||
self.base_url_var.set("https://dashscope.aliyuncs.com/compatible-mode/v1")
|
||||
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):
|
||||
self.ai_config_tab.grid_rowconfigure(i, weight=0)
|
||||
@@ -111,7 +114,8 @@ def build_ai_config_tab(self):
|
||||
|
||||
# 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))
|
||||
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.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 接口格式
|
||||
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_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")
|
||||
|
||||
|
||||
+13
-2
@@ -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.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.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))
|
||||
|
||||
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)
|
||||
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):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
|
||||
@@ -25,6 +25,11 @@ def generate_novel_architecture_ui(self):
|
||||
return
|
||||
|
||||
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)
|
||||
try:
|
||||
interface_format = self.interface_format_var.get().strip()
|
||||
@@ -39,6 +44,8 @@ def generate_novel_architecture_ui(self):
|
||||
genre = self.genre_var.get().strip()
|
||||
num_chapters = self.safe_get_int(self.num_chapters_var, 10)
|
||||
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("开始生成小说架构...")
|
||||
Novel_architecture_generate(
|
||||
@@ -53,7 +60,8 @@ def generate_novel_architecture_ui(self):
|
||||
filepath=filepath,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
timeout=timeout_val
|
||||
timeout=timeout_val,
|
||||
user_guidance=user_guidance # 添加内容指导参数
|
||||
)
|
||||
self.safe_log("✅ 小说架构生成完成。请在 'Novel Architecture' 标签页查看或编辑。")
|
||||
except Exception:
|
||||
@@ -69,6 +77,9 @@ def generate_chapter_blueprint_ui(self):
|
||||
return
|
||||
|
||||
def task():
|
||||
if not messagebox.askyesno("确认", "确定要生成章节草稿吗?"):
|
||||
self.enable_button_safe(self.btn_generate_chapter)
|
||||
return
|
||||
self.disable_button_safe(self.btn_generate_directory)
|
||||
try:
|
||||
interface_format = self.interface_format_var.get().strip()
|
||||
@@ -168,7 +179,64 @@ def generate_chapter_draft_ui(self):
|
||||
dialog.geometry("600x400")
|
||||
text_box = ctk.CTkTextbox(dialog, wrap="word", font=("Microsoft YaHei", 12))
|
||||
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.pack(pady=10)
|
||||
def on_confirm():
|
||||
@@ -236,6 +304,10 @@ def finalize_chapter_ui(self):
|
||||
return
|
||||
|
||||
def task():
|
||||
if not messagebox.askyesno("确认", "确定要定稿当前章节吗?"):
|
||||
self.enable_button_safe(self.btn_finalize_chapter)
|
||||
return
|
||||
|
||||
self.disable_button_safe(self.btn_finalize_chapter)
|
||||
try:
|
||||
interface_format = self.interface_format_var.get().strip()
|
||||
@@ -385,7 +457,12 @@ def import_knowledge_handler(self):
|
||||
self.handle_exception("导入知识库时出错")
|
||||
finally:
|
||||
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):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
|
||||
+12
-2
@@ -33,14 +33,24 @@ def build_left_layout(self):
|
||||
self.left_frame.grid_rowconfigure(4, weight=1)
|
||||
self.left_frame.columnconfigure(0, weight=1)
|
||||
|
||||
chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容 (可编辑)", font=("Microsoft YaHei", 12))
|
||||
chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||
self.chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容(可编辑) 字数:0", font=("Microsoft YaHei", 12))
|
||||
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))
|
||||
TextWidgetContextMenu(self.chapter_result)
|
||||
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 按钮区域
|
||||
self.step_buttons_frame = ctk.CTkFrame(self.left_frame)
|
||||
self.step_buttons_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
@@ -222,6 +224,120 @@ class NovelGeneratorGUI:
|
||||
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
|
||||
|
||||
+44
-9
@@ -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.grid(row=row_chap_num, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
# 6) 本章指导
|
||||
# 6) 内容指导
|
||||
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))
|
||||
TextWidgetContextMenu(self.user_guide_text)
|
||||
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) 可选元素:核心人物/关键道具/空间坐标/时间压力
|
||||
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))
|
||||
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
|
||||
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))
|
||||
@@ -85,20 +101,39 @@ def build_novel_params_area(self, start_row=1):
|
||||
def build_optional_buttons_area(self, start_row=2):
|
||||
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.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_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_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.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.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):
|
||||
frame = ctk.CTkFrame(parent)
|
||||
frame.grid(row=row, column=column, padx=padx, pady=pady, sticky=sticky)
|
||||
|
||||
+1528
File diff suppressed because it is too large
Load Diff
+13
-2
@@ -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.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.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))
|
||||
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):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
|
||||
+14
-2
@@ -11,17 +11,29 @@ def build_summary_tab(self):
|
||||
self.summary_tab.rowconfigure(0, weight=0)
|
||||
self.summary_tab.rowconfigure(1, 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.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.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))
|
||||
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):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
if not filepath:
|
||||
|
||||
Reference in New Issue
Block a user