From 8d2d66bd79ef4b834cf9f2e9f43dd2717333a7f2 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sat, 8 Feb 2025 00:03:50 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BC=88=E6=9C=AA=E6=B5=8B=E8=AF=95=EF=BC=89?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E4=BA=86=E6=96=AD=E7=82=B9=E7=BB=AD=E8=B7=91?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E6=89=A9=E5=86=99=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=94=B9=E4=B8=BA=E8=B4=A8=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. **在生成小说架构 (`Novel_architecture_generate`) 时**: - 新增了 `partial_architecture.json` 用来保存各步骤(核心种子、角色动力学、世界观、三幕式情节)已经生成的结果。 - 每完成一步,就将结果写入 `partial_architecture.json`;如果中途中断或失败了,下次调用该函数时,会直接跳过已完成的步骤,从失败的步骤继续执行。 - 全部完成后,会生成 `Novel_architecture.txt`,并删除 `partial_architecture.json`。 2. **在生成章节蓝图 (`Chapter_blueprint_generate`) 时**: - 如果 `Novel_directory.txt` **已有部分内容**,则说明之前已经生成了一部分。此时会从已完成的章节数继续往后生成,以实现**断点续跑**。 - 如果 `Novel_directory.txt`为空但**章节数少于一个阈值**(计算自 `chunk_size >= number_of_chapters`),则**一次性**生成,否则进行**分块生成**。 - 每完成一个分块,就把新生成的内容**追加**到 `Novel_directory.txt`(实际是整体覆盖写入,但包含已经生成的+新增的),以保证**中途出错**时不至于全部丢失。 --- main.spec | 4 +- novel_generator.py | 351 ++++++++++++++++++++++++------------------ prompt_definitions.py | 18 ++- ui.py | 51 ++++-- 4 files changed, 255 insertions(+), 169 deletions(-) diff --git a/main.spec b/main.spec index 0974bbb..17db507 100644 --- a/main.spec +++ b/main.spec @@ -45,7 +45,7 @@ exe = EXE( a.scripts, [], exclude_binaries=True, - name='AI_NovelGenerator_V1.4.0', + name='AI_NovelGenerator_V1.4.1', debug=True, bootloader_ignore_signals=False, strip=False, @@ -66,5 +66,5 @@ coll = COLLECT( strip=False, upx=True, upx_exclude=[], - name='AI_NovelGenerator_V1.4.0' + name='AI_NovelGenerator_V1.4.1' ) diff --git a/novel_generator.py b/novel_generator.py index ef02698..5192942 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -46,45 +46,6 @@ from embedding_adapters import create_embedding_adapter logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") - -# ============ 进度文件管理 ============ - -PROGRESS_FILE = "progress.json" - -def load_progress() -> dict: - """ - 简易进度文件读取,如果不存在则返回默认空字典。 - 你也可以在这里定制更多的进度信息。 - """ - if not os.path.exists(PROGRESS_FILE): - return { - "architecture_done": False, - "blueprint_done": False, - "blueprint_chunk_index": 1, # 若有分块生成,则记录当前分块的起始 - # 也可以记录已完成的章节 - "chapters_generated": [], # 已经生成草稿的章节列表 - "chapters_finalized": [] # 已经定稿的章节列表 - } - try: - with open(PROGRESS_FILE, "r", encoding="utf-8") as f: - return json.load(f) - except Exception: - return { - "architecture_done": False, - "blueprint_done": False, - "blueprint_chunk_index": 1, - "chapters_generated": [], - "chapters_finalized": [] - } - -def save_progress(progress: dict): - """ - 将进度写入到 progress.json 中。 - """ - with open(PROGRESS_FILE, "w", encoding="utf-8") as f: - json.dump(progress, f, ensure_ascii=False, indent=2) - - # ============ 通用的重试封装 ============ def call_with_retry(func, max_retries=3, sleep_time=2, fallback_return=None, **kwargs): @@ -183,7 +144,6 @@ def init_vector_store( documents = [Document(page_content=str(t)) for t in texts] - # 包一层try,如果embedding在初始化或插入过程中报错,则跳过 try: class LCEmbeddingWrapper(LCEmbeddings): def embed_documents(self, doc_texts: List[str]) -> List[List[float]]: @@ -454,6 +414,37 @@ def summarize_recent_chapters( return (short_summary, next_chapter_keywords) +# ============ 持久化:情节架构(partial_architecture.json) ============ + +def load_partial_architecture_data(filepath: str) -> dict: + """ + 从 filepath 下的 partial_architecture.json 读取已有的阶段性数据。 + 如果文件不存在或无法解析,返回空 dict。 + """ + partial_file = os.path.join(filepath, "partial_architecture.json") + if not os.path.exists(partial_file): + return {} + + try: + with open(partial_file, "r", encoding="utf-8") as f: + data = json.load(f) + return data + except Exception as e: + logging.warning(f"Failed to load partial_architecture.json: {e}") + return {} + +def save_partial_architecture_data(filepath: str, data: dict): + """ + 将阶段性数据写入 partial_architecture.json。 + """ + partial_file = os.path.join(filepath, "partial_architecture.json") + try: + with open(partial_file, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + logging.warning(f"Failed to save partial_architecture.json: {e}") + + # ============ 1) 生成总体架构 ============ def Novel_architecture_generate( @@ -476,16 +467,15 @@ def Novel_architecture_generate( 2. character_dynamics_prompt 3. world_building_prompt 4. plot_architecture_prompt + 若在中间任何一步报错且重试多次失败,则将已经生成的内容写入 partial_architecture.json 并退出; + 下次调用时可从该步骤继续。 最终输出 Novel_architecture.txt - 如果已生成,则不重复执行(利用 progress.json 中的标记)。 """ - progress = load_progress() - if progress.get("architecture_done", False): - logging.info("Novel architecture generation is already done. Skip.") - return - os.makedirs(filepath, exist_ok=True) + # 加载已有的阶段性数据 + partial_data = load_partial_architecture_data(filepath) + llm_adapter = create_llm_adapter( interface_format=interface_format, base_url=base_url, @@ -497,29 +487,77 @@ def Novel_architecture_generate( ) # Step1: 核心种子 - prompt_core = core_seed_prompt.format( - topic=topic, - genre=genre, - number_of_chapters=number_of_chapters, - word_number=word_number - ) - core_seed_result = invoke_with_cleaning(llm_adapter, prompt_core) + if "core_seed_result" not in partial_data: + logging.info("Step1: Generating core_seed_prompt (核心种子) ...") + prompt_core = core_seed_prompt.format( + topic=topic, + genre=genre, + number_of_chapters=number_of_chapters, + word_number=word_number + ) + core_seed_result = invoke_with_cleaning(llm_adapter, prompt_core) + if not core_seed_result.strip(): + # 多次重试依旧失败,则写入已完成内容后退出 + logging.warning("core_seed_prompt generation failed and returned empty.") + save_partial_architecture_data(filepath, partial_data) + return + partial_data["core_seed_result"] = core_seed_result + save_partial_architecture_data(filepath, partial_data) + else: + logging.info("Step1 already done. Skipping...") # Step2: 角色动力学 - prompt_character = character_dynamics_prompt.format(core_seed=core_seed_result.strip()) - character_dynamics_result = invoke_with_cleaning(llm_adapter, prompt_character) + 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()) + character_dynamics_result = invoke_with_cleaning(llm_adapter, prompt_character) + if not character_dynamics_result.strip(): + logging.warning("character_dynamics_prompt generation failed.") + # 写入目前已有结果,然后退出 + save_partial_architecture_data(filepath, partial_data) + return + partial_data["character_dynamics_result"] = character_dynamics_result + save_partial_architecture_data(filepath, partial_data) + else: + logging.info("Step2 already done. Skipping...") # Step3: 世界观 - prompt_world = world_building_prompt.format(core_seed=core_seed_result.strip()) - world_building_result = invoke_with_cleaning(llm_adapter, prompt_world) + 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()) + world_building_result = invoke_with_cleaning(llm_adapter, prompt_world) + if not world_building_result.strip(): + logging.warning("world_building_prompt generation failed.") + save_partial_architecture_data(filepath, partial_data) + return + partial_data["world_building_result"] = world_building_result + save_partial_architecture_data(filepath, partial_data) + else: + logging.info("Step3 already done. Skipping...") # Step4: 三幕式情节 - prompt_plot = plot_architecture_prompt.format( - core_seed=core_seed_result.strip(), - character_dynamics=character_dynamics_result.strip(), - world_building=world_building_result.strip() - ) - plot_arch_result = invoke_with_cleaning(llm_adapter, prompt_plot) + if "plot_arch_result" not in partial_data: + logging.info("Step4: Generating plot_architecture_prompt ...") + 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() + ) + plot_arch_result = invoke_with_cleaning(llm_adapter, prompt_plot) + if not plot_arch_result.strip(): + logging.warning("plot_architecture_prompt generation failed.") + save_partial_architecture_data(filepath, partial_data) + return + partial_data["plot_arch_result"] = plot_arch_result + save_partial_architecture_data(filepath, partial_data) + else: + logging.info("Step4 already done. Skipping...") + + # 如果能走到这里,说明全部步骤都完成了 + core_seed_result = partial_data["core_seed_result"] + character_dynamics_result = partial_data["character_dynamics_result"] + world_building_result = partial_data["world_building_result"] + plot_arch_result = partial_data["plot_arch_result"] final_content = ( "#=== 0) 小说设定 ===\n" @@ -539,9 +577,12 @@ def Novel_architecture_generate( save_string_to_txt(final_content, arch_file) logging.info("Novel_architecture.txt has been generated successfully.") - # 更新进度 - progress["architecture_done"] = True - save_progress(progress) + # 全部生成完成后,可以考虑删除 partial_architecture.json,或保留做追溯 + # 这里选择删除 + partial_arch_file = os.path.join(filepath, "partial_architecture.json") + if os.path.exists(partial_arch_file): + os.remove(partial_arch_file) + logging.info("partial_architecture.json removed (all steps completed).") # ============ 计算分块大小的工具函数 ============ @@ -555,9 +596,7 @@ def compute_chunk_size(number_of_chapters: int, max_tokens: int) -> int: """ tokens_per_chapter = 100.0 ratio = max_tokens / tokens_per_chapter # 例如:8192 / 100 = 81.92 - # 先取到最接近的10倍 ratio_rounded_to_10 = int(ratio // 10) * 10 # => 80 - # 再减10 chunk_size = ratio_rounded_to_10 - 10 # => 70 if chunk_size < 1: chunk_size = 1 @@ -566,7 +605,7 @@ def compute_chunk_size(number_of_chapters: int, max_tokens: int) -> int: return chunk_size -# ============ 2) 生成章节蓝图(新增分块逻辑) ============ +# ============ 2) 生成章节蓝图(新增分块逻辑 + 断点续跑) ============ def Chapter_blueprint_generate( interface_format: str, @@ -580,20 +619,13 @@ def Chapter_blueprint_generate( timeout: int = 600 ) -> None: """ - 如果章节数小于等于 chunk_size,则直接使用 chapter_blueprint_prompt 一次性生成。 - 如果章节数较多,则进行分块生成: - 1) 首先说明要生成的总章节数 - 2) 先生成 [1..chunk_size] 的章节 - 3) 将生成的文本作为已有目录传入,继续生成 [chunk_size+1..] 的章节 - 4) 最后汇总全部章节目录写入 Novel_directory.txt - - 过程中若发生错误,会进行一定次数重试;若仍失败则保留已生成的结果,方便下次中断续作。 + 若 Novel_directory.txt 已存在且内容非空,则表示可能是之前的部分生成结果; + 解析其中已有的章节数,从下一个章节继续分块生成; + 否则: + - 若章节数 <= chunk_size,直接一次性生成 + - 若章节数 > chunk_size,进行分块生成 + 生成完成后输出至 Novel_directory.txt。 """ - progress = load_progress() - if progress.get("blueprint_done", False): - logging.info("Chapter blueprint generation is already done. Skip.") - return - arch_file = os.path.join(filepath, "Novel_architecture.txt") if not os.path.exists(arch_file): logging.warning("Novel_architecture.txt not found. Please generate architecture first.") @@ -614,11 +646,65 @@ def Chapter_blueprint_generate( timeout=timeout ) - # 计算分块大小 + filename_dir = os.path.join(filepath, "Novel_directory.txt") + if not os.path.exists(filename_dir): + # 如果文件不存在,就先建一个空文件 + open(filename_dir, "w", encoding="utf-8").close() + + existing_blueprint = read_file(filename_dir).strip() chunk_size = compute_chunk_size(number_of_chapters, max_tokens) logging.info(f"Number of chapters = {number_of_chapters}, computed chunk_size = {chunk_size}.") - # 如果一次就可以生成全部 + # 如果已经有部分章节蓝图生成了,则进行断点续跑 + if existing_blueprint: + logging.info("Detected existing blueprint content. Will resume chunked generation from that point.") + + pattern = r"第\s*(\d+)\s*章" + existing_chapter_numbers = re.findall(pattern, existing_blueprint) + existing_chapter_numbers = [int(x) for x in existing_chapter_numbers if x.isdigit()] + + if existing_chapter_numbers: + max_existing_chap = max(existing_chapter_numbers) + else: + max_existing_chap = 0 + + logging.info(f"Existing blueprint indicates up to chapter {max_existing_chap} has been generated.") + + final_blueprint = existing_blueprint + current_start = max_existing_chap + 1 + while current_start <= number_of_chapters: + current_end = min(current_start + chunk_size - 1, number_of_chapters) + + chunk_prompt = chunked_chapter_blueprint_prompt.format( + novel_architecture=architecture_text, + chapter_list=final_blueprint, # 已有的章节列表文本 + number_of_chapters=number_of_chapters, + n=current_start, + m=current_end + ) + logging.info(f"Generating chapters [{current_start}..{current_end}] in a chunk...") + + chunk_result = invoke_with_cleaning(llm_adapter, chunk_prompt) + if not chunk_result.strip(): + logging.warning(f"Chunk generation for chapters [{current_start}..{current_end}] is empty.") + # 写入当前已经有的 final_blueprint,并结束 + clear_file_content(filename_dir) + save_string_to_txt(final_blueprint.strip(), filename_dir) + return + + final_blueprint += "\n\n" + chunk_result.strip() + + # 实时写入,以免中途崩溃造成丢失 + clear_file_content(filename_dir) + save_string_to_txt(final_blueprint.strip(), filename_dir) + + current_start = current_end + 1 + + logging.info("All chapters blueprint have been generated (resumed chunked).") + return + + # 如果 Novel_directory.txt 为空,则分情况: + # 1) 如果 chunk_size >= number_of_chapters,可以一次性生成 if chunk_size >= number_of_chapters: prompt = chapter_blueprint_prompt.format( novel_architecture=architecture_text, @@ -629,25 +715,21 @@ def Chapter_blueprint_generate( logging.warning("Chapter blueprint generation result is empty.") return - filename_dir = os.path.join(filepath, "Novel_directory.txt") clear_file_content(filename_dir) save_string_to_txt(blueprint_text, filename_dir) logging.info("Novel_directory.txt (chapter blueprint) has been generated successfully (single-shot).") - - progress["blueprint_done"] = True - save_progress(progress) return - # 否则,分块生成 + # 2) 如果 chunk_size < number_of_chapters,则进行分块生成 + logging.info("Will generate chapter blueprint in chunked mode from scratch.") final_blueprint = "" - current_start = progress.get("blueprint_chunk_index", 1) # 若之前中断,则从上一次的 chunk index 开始 + current_start = 1 while current_start <= number_of_chapters: current_end = min(current_start + chunk_size - 1, number_of_chapters) - # 分块提示 chunk_prompt = chunked_chapter_blueprint_prompt.format( novel_architecture=architecture_text, - chapter_list=final_blueprint, # 已有的章节列表文本 + chapter_list=final_blueprint, # 已有的章节列表文本 number_of_chapters=number_of_chapters, n=current_start, m=current_end @@ -657,34 +739,23 @@ def Chapter_blueprint_generate( chunk_result = invoke_with_cleaning(llm_adapter, chunk_prompt) if not chunk_result.strip(): logging.warning(f"Chunk generation for chapters [{current_start}..{current_end}] is empty.") - chunk_result = "" + # 写入已经生成的 final_blueprint + clear_file_content(filename_dir) + save_string_to_txt(final_blueprint.strip(), filename_dir) + return - # 将本次生成的文本拼接到最终结果中 if final_blueprint.strip(): - final_blueprint += "\n\n" + chunk_result + final_blueprint += "\n\n" + chunk_result.strip() else: - final_blueprint = chunk_result + final_blueprint = chunk_result.strip() - # 更新下一个块 - current_start = current_end + 1 - - # 将当前的 final_blueprint 写入文件,以便中断后保留 - filename_dir = os.path.join(filepath, "Novel_directory.txt") + # 实时写入,以免中途崩溃造成丢失 clear_file_content(filename_dir) save_string_to_txt(final_blueprint.strip(), filename_dir) - # 更新进度,以便中断后能接着来 - progress["blueprint_chunk_index"] = current_start - save_progress(progress) + current_start = current_end + 1 - if not final_blueprint.strip(): - logging.warning("All chunked generation results are empty, cannot create blueprint.") - return - - # 生成完成 logging.info("Novel_directory.txt (chapter blueprint) has been generated successfully (chunked).") - progress["blueprint_done"] = True - save_progress(progress) # ============ 3) 生成章节草稿 ============ @@ -715,16 +786,8 @@ def generate_chapter_draft( 根据 novel_number 判断是否为第一章。 - 若是第一章,则使用 first_chapter_draft_prompt - 否则使用 next_chapter_draft_prompt - 生成草稿后存入 chapters/chapter_{novel_number}.txt + 最终将生成文本存入 chapters/chapter_{novel_number}.txt。 """ - progress = load_progress() - if novel_number in progress.get("chapters_generated", []): - logging.info(f"Chapter {novel_number} draft already generated. Skip.") - # 直接返回已有内容 - chapters_dir = os.path.join(filepath, "chapters") - chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") - return read_file(chapter_file) - arch_file = os.path.join(filepath, "Novel_architecture.txt") novel_architecture_text = read_file(arch_file) @@ -751,11 +814,11 @@ def generate_chapter_draft( chapters_dir = os.path.join(filepath, "chapters") os.makedirs(chapters_dir, exist_ok=True) - # 根据是否是第一章,选择不同的 Prompt + # 判断是否为第一章 if novel_number == 1: - # 使用第一章提示词 prompt_text = first_chapter_draft_prompt.format( novel_number=novel_number, + word_number=word_number, chapter_title=chapter_title, chapter_role=chapter_role, chapter_purpose=chapter_purpose, @@ -773,7 +836,7 @@ def generate_chapter_draft( novel_setting=novel_architecture_text ) else: - # 若不是第一章,则先获取最近几章文本,并做摘要与检索 + # 若不是第一章,则获取最近几章文本,并做摘要与检索 recent_3_texts = get_last_n_chapters_text(chapters_dir, novel_number, n=3) short_summary, next_chapter_keywords = summarize_recent_chapters( interface_format=interface_format, @@ -786,17 +849,18 @@ def generate_chapter_draft( timeout=timeout ) - # 从最近章节中获取最后一段内容作为前章结尾 + # 从最近章节中获取最后一段作为前章结尾 previous_chapter_excerpt = "" for text_block in reversed(recent_3_texts): if text_block.strip(): + # 取后1500字符左右 if len(text_block) > 1500: previous_chapter_excerpt = text_block[-1500:] else: previous_chapter_excerpt = text_block break - # 从向量库检索上下文(若失败则为空,不中断) + # 从向量库检索上下文 embedding_adapter = create_embedding_adapter( embedding_interface_format, embedding_api_key, @@ -813,9 +877,9 @@ def generate_chapter_draft( if not relevant_context.strip(): relevant_context = "(无检索到的上下文)" - # 使用后续章节提示词 prompt_text = next_chapter_draft_prompt.format( novel_number=novel_number, + word_number=word_number, chapter_title=chapter_title, chapter_role=chapter_role, chapter_purpose=chapter_purpose, @@ -837,7 +901,6 @@ def generate_chapter_draft( previous_chapter_excerpt=previous_chapter_excerpt ) - # 调用LLM生成 llm_adapter = create_llm_adapter( interface_format=interface_format, base_url=base_url, @@ -847,21 +910,16 @@ def generate_chapter_draft( max_tokens=max_tokens, timeout=timeout ) + chapter_content = invoke_with_cleaning(llm_adapter, prompt_text) if not chapter_content.strip(): logging.warning("Generated chapter draft is empty.") - # 保存章节文本 chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") clear_file_content(chapter_file) save_string_to_txt(chapter_content, chapter_file) logging.info(f"[Draft] Chapter {novel_number} generated as a draft.") - - # 更新进度 - progress["chapters_generated"].append(novel_number) - save_progress(progress) - return chapter_content @@ -883,11 +941,10 @@ def finalize_chapter( max_tokens: int, timeout: int = 600 ): - progress = load_progress() - if novel_number in progress.get("chapters_finalized", []): - logging.info(f"Chapter {novel_number} is already finalized. Skip.") - return - + """ + 对指定章节做最终处理:更新全局摘要、更新角色状态、插入向量库等。 + 默认无需再做扩写操作,若有需要可在外部调用 enrich_chapter_text 处理后再定稿。 + """ chapters_dir = os.path.join(filepath, "chapters") chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") chapter_text = read_file(chapter_file).strip() @@ -895,14 +952,10 @@ def finalize_chapter( logging.warning(f"Chapter {novel_number} is empty, cannot finalize.") return - # 如果内容过短,则尝试扩写 - if len(chapter_text) < 0.7 * word_number: - chapter_text = enrich_chapter_text(chapter_text, word_number, api_key, base_url, model_name, temperature, interface_format, max_tokens, timeout) - clear_file_content(chapter_file) - save_string_to_txt(chapter_text, chapter_file) - + # 进行摘要、角色状态更新 global_summary_file = os.path.join(filepath, "global_summary.txt") old_global_summary = read_file(global_summary_file) + character_state_file = os.path.join(filepath, "character_state.txt") old_character_state = read_file(character_state_file) @@ -915,6 +968,7 @@ def finalize_chapter( max_tokens=max_tokens, timeout=timeout ) + prompt_summary = summary_prompt.format( chapter_text=chapter_text, global_summary=old_global_summary @@ -937,7 +991,7 @@ def finalize_chapter( clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) - # 更新向量库(若失败则跳过) + # 更新向量库 embedding_adapter = create_embedding_adapter( embedding_interface_format, embedding_api_key, @@ -948,10 +1002,6 @@ def finalize_chapter( logging.info(f"Chapter {novel_number} has been finalized.") - # 更新进度 - progress["chapters_finalized"].append(novel_number) - save_progress(progress) - def enrich_chapter_text( chapter_text: str, @@ -964,6 +1014,9 @@ def enrich_chapter_text( max_tokens: int, timeout: int=600 ) -> str: + """ + 对章节文本进行扩写,使其更接近 word_number 字数,保持剧情连贯。 + """ llm_adapter = create_llm_adapter( interface_format=interface_format, base_url=base_url, diff --git a/prompt_definitions.py b/prompt_definitions.py index a6b2d2b..e32745d 100644 --- a/prompt_definitions.py +++ b/prompt_definitions.py @@ -306,7 +306,7 @@ first_chapter_draft_prompt = """\ - 小说设定: {novel_setting} -请完成第 {novel_number} 章的正文,至少设计下方2个具有动态张力的场景: +请完成第 {novel_number} 章的正文,字数要求{word_number}字,至少设计下方2个或以上具有动态张力的场景: 1. 对话场景: - 潜台词冲突(表面谈论A,实际博弈B) - 权力关系变化(通过非对称对话长度体现) @@ -322,6 +322,12 @@ first_chapter_draft_prompt = """\ - 隐喻系统的运用(连接世界观符号) - 决策前的价值天平描写 +4. 环境场景: + - 空间透视变化(宏观→微观→异常焦点) + - 非常规感官组合(如"听见阳光的重量") + - 动态环境反映心理(环境与人物心理对应) + - 隐藏线索植入(环境暗示未来事件) + 文末设置一个"钩链转折":结尾时回收旧悬念/创造新悬念/抛出新危机/颠覆某个认知/神转折等。 格式要求: @@ -364,7 +370,9 @@ next_chapter_draft_prompt = """\ 前章结尾段: {previous_chapter_excerpt} -请依据前章结尾片段,继续完成第 {novel_number} 章的正文,至少设计下方2个具有动态张力的场景: +请参考前章结尾片段,继续完成第 {novel_number} 章的正文,字数要求{word_number}字,确保与前章结尾衔接流畅, + +本章至少设计下方2个或以上具有动态张力的场景: 1. 对话场景: - 潜台词冲突(表面谈论A,实际博弈B) - 权力关系变化(通过非对称对话长度体现) @@ -380,6 +388,12 @@ next_chapter_draft_prompt = """\ - 隐喻系统的运用(连接世界观符号) - 决策前的价值天平描写 +4. 环境场景: + - 空间透视变化(宏观→微观→异常焦点) + - 非常规感官组合(如"听见阳光的重量") + - 动态环境反映心理(环境与人物心理对应) + - 隐藏线索植入(环境暗示未来事件) + 文末设置一个"钩链转折":结尾时回收旧悬念/创造新悬念/抛出新危机/颠覆某个认知/神转折等。 格式要求: diff --git a/ui.py b/ui.py index 35bdd16..a9845b8 100644 --- a/ui.py +++ b/ui.py @@ -19,7 +19,8 @@ from novel_generator import ( finalize_chapter, import_knowledge_file, clear_vector_store, - get_last_n_chapters_text + get_last_n_chapters_text, + enrich_chapter_text ) from consistency_checker import check_consistency @@ -105,7 +106,6 @@ class NovelGeneratorGUI: self.model_name_var = ctk.StringVar(value=self.loaded_config.get("model_name", "gpt-4o-mini")) self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7)) self.max_tokens_var = ctk.IntVar(value=self.loaded_config.get("max_tokens", 8192)) - # === New: Timeout === self.timeout_var = ctk.IntVar(value=self.loaded_config.get("timeout", 600)) # Embedding相关 @@ -265,16 +265,12 @@ class NovelGeneratorGUI: self.build_ai_config_tab() self.build_embeddings_config_tab() - # 封装一个小工具函数,用来创建「标签 + 问号按钮」的组合 def create_label_with_help(self, parent, label_text, tooltip_key, row, column, font=None, sticky="e", padx=5, pady=5): - # frame容器:同一格子里存放 label + "?"按钮 frame = ctk.CTkFrame(parent) frame.grid(row=row, column=column, padx=padx, pady=pady, sticky=sticky) frame.columnconfigure(0, weight=0) - # 先放 label label = ctk.CTkLabel(frame, text=label_text, font=font) label.pack(side="left") - # 再放问号按钮 btn = ctk.CTkButton( frame, text="?", @@ -418,7 +414,6 @@ class NovelGeneratorGUI: self.max_tokens_value_label.grid(row=5, column=2, padx=5, pady=5, sticky="w") # 7) Timeout (sec) - # === MODIFIED: 使用Slider替换Entry === self.create_label_with_help( parent=self.ai_config_tab, label_text="Timeout (sec):", @@ -435,7 +430,7 @@ class NovelGeneratorGUI: timeout_slider = ctk.CTkSlider( self.ai_config_tab, from_=0, - to=3600, # 设定一个合理上限,例如1小时 + to=3600, number_of_steps=3600, command=update_timeout_label, variable=self.timeout_var @@ -448,7 +443,6 @@ class NovelGeneratorGUI: font=("Microsoft YaHei", 12) ) self.timeout_value_label.grid(row=6, column=2, padx=5, pady=5, sticky="w") - # === MODIFIED END === def build_embeddings_config_tab(self): def on_embedding_interface_changed(new_value): @@ -585,7 +579,6 @@ class NovelGeneratorGUI: chapter_word_frame.grid(row=row_for_chapter_and_word, column=1, padx=5, pady=5, sticky="ew") chapter_word_frame.columnconfigure((0, 1, 2, 3), weight=0) - # 左边标签 label_frame = self.create_label_with_help( parent=self.params_frame, label_text="章节数 & 每章字数:", @@ -595,7 +588,6 @@ class NovelGeneratorGUI: font=("Microsoft YaHei", 12) ) - # 输入框 num_chapters_label = ctk.CTkLabel(chapter_word_frame, text="章节数:", font=("Microsoft YaHei", 12)) num_chapters_label.grid(row=0, column=0, padx=5, pady=5, sticky="e") num_chapters_entry = ctk.CTkEntry(chapter_word_frame, textvariable=self.num_chapters_var, width=60, font=("Microsoft YaHei", 12)) @@ -655,7 +647,6 @@ class NovelGeneratorGUI: # 7) 可选元素:核心人物/关键道具/空间坐标/时间压力 row_idx = 6 - # 核心人物 self.create_label_with_help( parent=self.params_frame, label_text="核心人物:", @@ -668,7 +659,6 @@ class NovelGeneratorGUI: char_inv_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") row_idx += 1 - # 关键道具 self.create_label_with_help( parent=self.params_frame, label_text="关键道具:", @@ -681,7 +671,6 @@ class NovelGeneratorGUI: key_items_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") row_idx += 1 - # 空间坐标 self.create_label_with_help( parent=self.params_frame, label_text="空间坐标:", @@ -694,7 +683,6 @@ class NovelGeneratorGUI: scene_loc_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") row_idx += 1 - # 时间压力 self.create_label_with_help( parent=self.params_frame, label_text="时间压力:", @@ -1011,13 +999,44 @@ class NovelGeneratorGUI: word_number = self.safe_get_int(self.word_number_var, 3000) self.safe_log(f"开始定稿第{chap_num}章...") + + # 先读取用户在文本框中编辑好的内容 chapters_dir = os.path.join(filepath, "chapters") os.makedirs(chapters_dir, exist_ok=True) chapter_file = os.path.join(chapters_dir, f"chapter_{chap_num}.txt") + edited_text = self.chapter_result.get("0.0", "end").strip() + + # 如果字数不足70%,询问是否扩写 + if len(edited_text) < 0.7 * word_number: + ask = messagebox.askyesno( + "字数不足", + f"当前章节字数 ({len(edited_text)}) 低于目标字数({word_number})的70%,是否要尝试扩写?" + ) + if ask: + # 调用 enrich_chapter_text 进行扩写 + self.safe_log("正在扩写章节内容...") + enriched = enrich_chapter_text( + chapter_text=edited_text, + word_number=word_number, + api_key=api_key, + base_url=base_url, + model_name=model_name, + temperature=temperature, + interface_format=interface_format, + max_tokens=max_tokens, + timeout=timeout_val + ) + edited_text = enriched + # 更新文本框显示 + self.master.after(0, lambda: self.chapter_result.delete("0.0", "end")) + self.master.after(0, lambda: self.chapter_result.insert("0.0", edited_text)) + + # 将(可能已扩写的)文本保存到本地文件 clear_file_content(chapter_file) save_string_to_txt(edited_text, chapter_file) + # 调用 finalize_chapter 做最终处理 finalize_chapter( novel_number=chap_num, word_number=word_number, @@ -1163,7 +1182,7 @@ class NovelGeneratorGUI: text_area.insert("0.0", arcs_text) text_area.configure(state="disabled") - # ============ 其余标签页: Novel Architecture, Chapter Blueprint, Character State, Summary ============ + # ============ 其余标签页 ============ def build_setting_tab(self): self.setting_tab.rowconfigure(0, weight=0) self.setting_tab.rowconfigure(1, weight=1)