+69
-15
@@ -35,7 +35,8 @@ from prompt_definitions import (
|
|||||||
update_character_state_prompt,
|
update_character_state_prompt,
|
||||||
first_chapter_draft_prompt,
|
first_chapter_draft_prompt,
|
||||||
next_chapter_draft_prompt,
|
next_chapter_draft_prompt,
|
||||||
summarize_recent_chapters_prompt
|
summarize_recent_chapters_prompt,
|
||||||
|
create_character_state_prompt
|
||||||
)
|
)
|
||||||
|
|
||||||
# 章节目录解析
|
# 章节目录解析
|
||||||
@@ -146,20 +147,20 @@ def init_vector_store(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
class LCEmbeddingWrapper(LCEmbeddings):
|
class LCEmbeddingWrapper(LCEmbeddings):
|
||||||
def embed_documents(self, doc_texts: List[str]) -> List[List[float]]:
|
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||||
return call_with_retry(
|
return call_with_retry(
|
||||||
func=embedding_adapter.embed_documents,
|
func=embedding_adapter.embed_documents,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
fallback_return=[],
|
fallback_return=[],
|
||||||
doc_texts=doc_texts
|
texts=texts
|
||||||
)
|
)
|
||||||
|
|
||||||
def embed_query(self, query_text: str) -> List[float]:
|
def embed_query(self, query: str) -> List[float]:
|
||||||
res = call_with_retry(
|
res = call_with_retry(
|
||||||
func=embedding_adapter.embed_query,
|
func=embedding_adapter.embed_query,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
fallback_return=[],
|
fallback_return=[],
|
||||||
query_text=query_text
|
query=query
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -195,20 +196,20 @@ def load_vector_store(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
class LCEmbeddingWrapper(LCEmbeddings):
|
class LCEmbeddingWrapper(LCEmbeddings):
|
||||||
def embed_documents(self, doc_texts: List[str]) -> List[List[float]]:
|
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||||
return call_with_retry(
|
return call_with_retry(
|
||||||
func=embedding_adapter.embed_documents,
|
func=embedding_adapter.embed_documents,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
fallback_return=[],
|
fallback_return=[],
|
||||||
doc_texts=doc_texts
|
texts=texts
|
||||||
)
|
)
|
||||||
|
|
||||||
def embed_query(self, query_text: str) -> List[float]:
|
def embed_query(self, query: str) -> List[float]:
|
||||||
res = call_with_retry(
|
res = call_with_retry(
|
||||||
func=embedding_adapter.embed_query,
|
func=embedding_adapter.embed_query,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
fallback_return=[],
|
fallback_return=[],
|
||||||
query_text=query_text
|
query=query
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -249,6 +250,7 @@ def split_text_for_vectorstore(chapter_text: str,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
nltk.download('punkt', quiet=True)
|
nltk.download('punkt', quiet=True)
|
||||||
|
nltk.download('punkt_tab', quiet=True)
|
||||||
sentences = nltk.sent_tokenize(chapter_text)
|
sentences = nltk.sent_tokenize(chapter_text)
|
||||||
if not sentences:
|
if not sentences:
|
||||||
return []
|
return []
|
||||||
@@ -331,6 +333,7 @@ def get_relevant_context_from_vector_store(
|
|||||||
"""
|
"""
|
||||||
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
|
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
|
||||||
如果向量库加载/检索失败,则返回空字符串。
|
如果向量库加载/检索失败,则返回空字符串。
|
||||||
|
最终只返回最多2000字符的检索片段。
|
||||||
"""
|
"""
|
||||||
store = load_vector_store(embedding_adapter, filepath)
|
store = load_vector_store(embedding_adapter, filepath)
|
||||||
if not store:
|
if not store:
|
||||||
@@ -343,6 +346,9 @@ def get_relevant_context_from_vector_store(
|
|||||||
logging.info(f"No relevant documents found for query '{query}'. Returning empty context.")
|
logging.info(f"No relevant documents found for query '{query}'. Returning empty context.")
|
||||||
return ""
|
return ""
|
||||||
combined = "\n".join([d.page_content for d in docs])
|
combined = "\n".join([d.page_content for d in docs])
|
||||||
|
# 限制长度最多2000字符
|
||||||
|
if len(combined) > 2000:
|
||||||
|
combined = combined[:2000]
|
||||||
return combined
|
return combined
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Similarity search failed: {e}")
|
logging.warning(f"Similarity search failed: {e}")
|
||||||
@@ -470,6 +476,10 @@ def Novel_architecture_generate(
|
|||||||
若在中间任何一步报错且重试多次失败,则将已经生成的内容写入 partial_architecture.json 并退出;
|
若在中间任何一步报错且重试多次失败,则将已经生成的内容写入 partial_architecture.json 并退出;
|
||||||
下次调用时可从该步骤继续。
|
下次调用时可从该步骤继续。
|
||||||
最终输出 Novel_architecture.txt
|
最终输出 Novel_architecture.txt
|
||||||
|
|
||||||
|
新增:
|
||||||
|
- 在完成角色动力学设定后,依据该角色体系,使用 create_character_state_prompt 生成初始角色状态表,
|
||||||
|
并存储到 character_state.txt,后续维护更新。
|
||||||
"""
|
"""
|
||||||
os.makedirs(filepath, exist_ok=True)
|
os.makedirs(filepath, exist_ok=True)
|
||||||
|
|
||||||
@@ -521,6 +531,28 @@ def Novel_architecture_generate(
|
|||||||
else:
|
else:
|
||||||
logging.info("Step2 already done. Skipping...")
|
logging.info("Step2 already done. Skipping...")
|
||||||
|
|
||||||
|
# 在完成角色动力学设定后,生成初始角色状态表
|
||||||
|
if "character_dynamics_result" in partial_data and "character_state_result" not in partial_data:
|
||||||
|
logging.info("Generating initial character state from character dynamics ...")
|
||||||
|
prompt_char_state_init = create_character_state_prompt.format(
|
||||||
|
character_dynamics=partial_data["character_dynamics_result"].strip()
|
||||||
|
)
|
||||||
|
character_state_init = invoke_with_cleaning(llm_adapter, prompt_char_state_init)
|
||||||
|
if not character_state_init.strip():
|
||||||
|
logging.warning("create_character_state_prompt generation failed.")
|
||||||
|
# 写入目前已有结果,然后退出
|
||||||
|
save_partial_architecture_data(filepath, partial_data)
|
||||||
|
return
|
||||||
|
|
||||||
|
partial_data["character_state_result"] = character_state_init
|
||||||
|
# 保存到文件
|
||||||
|
character_state_file = os.path.join(filepath, "character_state.txt")
|
||||||
|
clear_file_content(character_state_file)
|
||||||
|
save_string_to_txt(character_state_init, character_state_file)
|
||||||
|
|
||||||
|
save_partial_architecture_data(filepath, partial_data)
|
||||||
|
logging.info("Initial character state created and saved.")
|
||||||
|
|
||||||
# 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 ...")
|
||||||
@@ -595,9 +627,9 @@ def compute_chunk_size(number_of_chapters: int, max_tokens: int) -> int:
|
|||||||
并确保 chunk_size 不会小于1或大于实际章节数。
|
并确保 chunk_size 不会小于1或大于实际章节数。
|
||||||
"""
|
"""
|
||||||
tokens_per_chapter = 100.0
|
tokens_per_chapter = 100.0
|
||||||
ratio = max_tokens / tokens_per_chapter # 例如:8192 / 100 = 81.92
|
ratio = max_tokens / tokens_per_chapter
|
||||||
ratio_rounded_to_10 = int(ratio // 10) * 10 # => 80
|
ratio_rounded_to_10 = int(ratio // 10) * 10
|
||||||
chunk_size = ratio_rounded_to_10 - 10 # => 70
|
chunk_size = ratio_rounded_to_10 - 10
|
||||||
if chunk_size < 1:
|
if chunk_size < 1:
|
||||||
chunk_size = 1
|
chunk_size = 1
|
||||||
if chunk_size > number_of_chapters:
|
if chunk_size > number_of_chapters:
|
||||||
@@ -605,6 +637,22 @@ def compute_chunk_size(number_of_chapters: int, max_tokens: int) -> int:
|
|||||||
return chunk_size
|
return chunk_size
|
||||||
|
|
||||||
|
|
||||||
|
def limit_chapter_blueprint(blueprint_text: str, limit_chapters: int = 100) -> str:
|
||||||
|
"""
|
||||||
|
从已有章节目录中只取最近的 limit_chapters 章,以避免 prompt 超长。
|
||||||
|
"""
|
||||||
|
pattern = r"(第\s*\d+\s*章.*?)(?=第\s*\d+\s*章|$)"
|
||||||
|
chapters = re.findall(pattern, blueprint_text, flags=re.DOTALL)
|
||||||
|
if not chapters:
|
||||||
|
return blueprint_text
|
||||||
|
|
||||||
|
if len(chapters) <= limit_chapters:
|
||||||
|
return blueprint_text
|
||||||
|
|
||||||
|
selected = chapters[-limit_chapters:]
|
||||||
|
return "\n\n".join(selected).strip()
|
||||||
|
|
||||||
|
|
||||||
# ============ 2) 生成章节蓝图(新增分块逻辑 + 断点续跑) ============
|
# ============ 2) 生成章节蓝图(新增分块逻辑 + 断点续跑) ============
|
||||||
|
|
||||||
def Chapter_blueprint_generate(
|
def Chapter_blueprint_generate(
|
||||||
@@ -621,6 +669,7 @@ def Chapter_blueprint_generate(
|
|||||||
"""
|
"""
|
||||||
若 Novel_directory.txt 已存在且内容非空,则表示可能是之前的部分生成结果;
|
若 Novel_directory.txt 已存在且内容非空,则表示可能是之前的部分生成结果;
|
||||||
解析其中已有的章节数,从下一个章节继续分块生成;
|
解析其中已有的章节数,从下一个章节继续分块生成;
|
||||||
|
对于已有章节目录,传入时仅保留最近100章目录,避免prompt过长。
|
||||||
否则:
|
否则:
|
||||||
- 若章节数 <= chunk_size,直接一次性生成
|
- 若章节数 <= chunk_size,直接一次性生成
|
||||||
- 若章节数 > chunk_size,进行分块生成
|
- 若章节数 > chunk_size,进行分块生成
|
||||||
@@ -674,10 +723,11 @@ def Chapter_blueprint_generate(
|
|||||||
current_start = max_existing_chap + 1
|
current_start = max_existing_chap + 1
|
||||||
while current_start <= number_of_chapters:
|
while current_start <= number_of_chapters:
|
||||||
current_end = min(current_start + chunk_size - 1, number_of_chapters)
|
current_end = min(current_start + chunk_size - 1, number_of_chapters)
|
||||||
|
limited_blueprint = limit_chapter_blueprint(final_blueprint, 100)
|
||||||
|
|
||||||
chunk_prompt = chunked_chapter_blueprint_prompt.format(
|
chunk_prompt = chunked_chapter_blueprint_prompt.format(
|
||||||
novel_architecture=architecture_text,
|
novel_architecture=architecture_text,
|
||||||
chapter_list=final_blueprint, # 已有的章节列表文本
|
chapter_list=limited_blueprint, # 只保留最近100章
|
||||||
number_of_chapters=number_of_chapters,
|
number_of_chapters=number_of_chapters,
|
||||||
n=current_start,
|
n=current_start,
|
||||||
m=current_end
|
m=current_end
|
||||||
@@ -694,7 +744,7 @@ def Chapter_blueprint_generate(
|
|||||||
|
|
||||||
final_blueprint += "\n\n" + chunk_result.strip()
|
final_blueprint += "\n\n" + chunk_result.strip()
|
||||||
|
|
||||||
# 实时写入,以免中途崩溃造成丢失
|
# 实时写入
|
||||||
clear_file_content(filename_dir)
|
clear_file_content(filename_dir)
|
||||||
save_string_to_txt(final_blueprint.strip(), filename_dir)
|
save_string_to_txt(final_blueprint.strip(), filename_dir)
|
||||||
|
|
||||||
@@ -726,10 +776,11 @@ def Chapter_blueprint_generate(
|
|||||||
current_start = 1
|
current_start = 1
|
||||||
while current_start <= number_of_chapters:
|
while current_start <= number_of_chapters:
|
||||||
current_end = min(current_start + chunk_size - 1, number_of_chapters)
|
current_end = min(current_start + chunk_size - 1, number_of_chapters)
|
||||||
|
limited_blueprint = limit_chapter_blueprint(final_blueprint, 100)
|
||||||
|
|
||||||
chunk_prompt = chunked_chapter_blueprint_prompt.format(
|
chunk_prompt = chunked_chapter_blueprint_prompt.format(
|
||||||
novel_architecture=architecture_text,
|
novel_architecture=architecture_text,
|
||||||
chapter_list=final_blueprint, # 已有的章节列表文本
|
chapter_list=limited_blueprint, # 只保留最近100章
|
||||||
number_of_chapters=number_of_chapters,
|
number_of_chapters=number_of_chapters,
|
||||||
n=current_start,
|
n=current_start,
|
||||||
m=current_end
|
m=current_end
|
||||||
@@ -969,6 +1020,7 @@ def finalize_chapter(
|
|||||||
timeout=timeout
|
timeout=timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 更新全局摘要
|
||||||
prompt_summary = summary_prompt.format(
|
prompt_summary = summary_prompt.format(
|
||||||
chapter_text=chapter_text,
|
chapter_text=chapter_text,
|
||||||
global_summary=old_global_summary
|
global_summary=old_global_summary
|
||||||
@@ -977,6 +1029,7 @@ def finalize_chapter(
|
|||||||
if not new_global_summary.strip():
|
if not new_global_summary.strip():
|
||||||
new_global_summary = old_global_summary
|
new_global_summary = old_global_summary
|
||||||
|
|
||||||
|
# 更新角色状态
|
||||||
prompt_char_state = update_character_state_prompt.format(
|
prompt_char_state = update_character_state_prompt.format(
|
||||||
chapter_text=chapter_text,
|
chapter_text=chapter_text,
|
||||||
old_state=old_character_state
|
old_state=old_character_state
|
||||||
@@ -1040,6 +1093,7 @@ def advanced_split_content(content: str,
|
|||||||
similarity_threshold: float = 0.7,
|
similarity_threshold: float = 0.7,
|
||||||
max_length: int = 500) -> List[str]:
|
max_length: int = 500) -> List[str]:
|
||||||
nltk.download('punkt', quiet=True)
|
nltk.download('punkt', quiet=True)
|
||||||
|
nltk.download('punkt_tab', quiet=True)
|
||||||
sentences = nltk.sent_tokenize(content)
|
sentences = nltk.sent_tokenize(content)
|
||||||
if not sentences:
|
if not sentences:
|
||||||
return []
|
return []
|
||||||
|
|||||||
+51
-5
@@ -168,7 +168,7 @@ chunked_chapter_blueprint_prompt = """\
|
|||||||
|
|
||||||
需要生成总共{number_of_chapters}章的节奏分布,
|
需要生成总共{number_of_chapters}章的节奏分布,
|
||||||
|
|
||||||
当前已有章节目录(若未空则说明是初始生成):\n
|
当前已有章节目录(若为空则说明是初始生成):\n
|
||||||
{chapter_list}
|
{chapter_list}
|
||||||
|
|
||||||
现在请设计第{n}章到第{m}的节奏分布:
|
现在请设计第{n}章到第{m}的节奏分布:
|
||||||
@@ -228,14 +228,60 @@ summary_prompt = """\
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 7. 角色状态更新 ===================
|
# =============== 7. 角色状态更新 ===================
|
||||||
|
create_character_state_prompt = """\
|
||||||
|
依据当前角色动力学设定:{character_dynamics}
|
||||||
|
|
||||||
|
请生成一个角色状态文档,内容格式:
|
||||||
|
角色A属性:
|
||||||
|
├──物品:
|
||||||
|
├──物品(若有初始物品则增加,没有则为暂无):描述
|
||||||
|
...
|
||||||
|
├──能力
|
||||||
|
├──技能1(若有初始技能则增加,没有则为暂无):描述
|
||||||
|
...
|
||||||
|
├──状态
|
||||||
|
├──身体状态:
|
||||||
|
├──Buff/Debuff
|
||||||
|
├──心理状态:描述
|
||||||
|
|
||||||
|
├──主要角色间关系网
|
||||||
|
├──角色B:描述(初始有关联则增加,没有则为暂无关系)
|
||||||
|
├──角色C:描述(初始有关联则增加,没有则为暂无关系)
|
||||||
|
...
|
||||||
|
├──触发或加深的事件
|
||||||
|
├──暂无事件
|
||||||
|
...
|
||||||
|
|
||||||
|
角色B属性:
|
||||||
|
├──物品
|
||||||
|
├──...
|
||||||
|
├──能力
|
||||||
|
├──...
|
||||||
|
├──状态
|
||||||
|
├──...
|
||||||
|
├──主要角色间关系网
|
||||||
|
├──...
|
||||||
|
├──触发或加深的事件
|
||||||
|
├──...
|
||||||
|
|
||||||
|
角色C属性:
|
||||||
|
......
|
||||||
|
|
||||||
|
新出场角色:
|
||||||
|
- (此处填写未来任何新增角色或临时出场人物的基本信息)
|
||||||
|
|
||||||
|
要求:
|
||||||
|
仅返回编写好的角色状态文本,不要解释任何内容。
|
||||||
|
"""
|
||||||
|
|
||||||
update_character_state_prompt = """\
|
update_character_state_prompt = """\
|
||||||
以下是新完成的章节文本:
|
以下是新完成的章节文本:
|
||||||
{chapter_text}
|
{chapter_text}
|
||||||
|
|
||||||
这是当前的角色状态文档(可为空):
|
这是当前的角色状态文档:
|
||||||
{old_state}
|
{old_state}
|
||||||
|
|
||||||
请更新角色状态,内容格式:
|
请更新主要角色状态,内容格式:
|
||||||
角色A属性:
|
角色A属性:
|
||||||
├──物品:
|
├──物品:
|
||||||
├──某物(道具):描述
|
├──某物(道具):描述
|
||||||
@@ -275,7 +321,7 @@ update_character_state_prompt = """\
|
|||||||
......
|
......
|
||||||
|
|
||||||
新出场角色:
|
新出场角色:
|
||||||
- 任何新增角色或临时出场人物的基本信息
|
- 任何新增角色或临时出场人物的基本信息,简要描述即可,不要展开,淡出视线的角色可删除。
|
||||||
|
|
||||||
要求:
|
要求:
|
||||||
- 请直接在已有文档基础上进行增删
|
- 请直接在已有文档基础上进行增删
|
||||||
@@ -370,7 +416,7 @@ next_chapter_draft_prompt = """\
|
|||||||
前章结尾段:
|
前章结尾段:
|
||||||
{previous_chapter_excerpt}
|
{previous_chapter_excerpt}
|
||||||
|
|
||||||
请参考前章结尾片段,继续完成第 {novel_number} 章的正文,字数要求{word_number}字,确保与前章结尾衔接流畅,
|
依据前章结尾剧情,开始完成第 {novel_number} 章的正文,字数要求{word_number}字,确保与前章结尾衔接流畅,
|
||||||
|
|
||||||
本章至少设计下方2个或以上具有动态张力的场景:
|
本章至少设计下方2个或以上具有动态张力的场景:
|
||||||
1. 对话场景:
|
1. 对话场景:
|
||||||
|
|||||||
Reference in New Issue
Block a user