#novel_generator/architecture.py # -*- coding: utf-8 -*- """ 小说总体架构生成(Novel_architecture_generate 及相关辅助函数) """ import os import json import logging import traceback from novel_generator.common import invoke_with_cleaning from llm_adapters import create_llm_adapter from prompt_definitions import ( core_seed_prompt, character_dynamics_prompt, world_building_prompt, plot_architecture_prompt, create_character_state_prompt ) from utils import clear_file_content, save_string_to_txt 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}") def Novel_architecture_generate( interface_format: str, api_key: str, base_url: str, llm_model: str, topic: str, genre: str, number_of_chapters: int, word_number: int, filepath: str, temperature: float = 0.7, max_tokens: int = 2048, timeout: int = 600 ) -> None: """ 依次调用: 1. core_seed_prompt 2. character_dynamics_prompt 3. world_building_prompt 4. plot_architecture_prompt 若在中间任何一步报错且重试多次失败,则将已经生成的内容写入 partial_architecture.json 并退出; 下次调用时可从该步骤继续。 最终输出 Novel_architecture.txt 新增: - 在完成角色动力学设定后,依据该角色体系,使用 create_character_state_prompt 生成初始角色状态表, 并存储到 character_state.txt,后续维护更新。 """ 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, model_name=llm_model, api_key=api_key, temperature=temperature, max_tokens=max_tokens, timeout=timeout ) # Step1: 核心种子 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: 角色动力学 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...") # 生成初始角色状态 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: 世界观 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: 三幕式情节 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" f"主题:{topic},类型:{genre},篇幅:约{number_of_chapters}章(每章{word_number}字)\n\n" "#=== 1) 核心种子 ===\n" f"{core_seed_result}\n\n" "#=== 2) 角色动力学 ===\n" f"{character_dynamics_result}\n\n" "#=== 3) 世界观 ===\n" f"{world_building_result}\n\n" "#=== 4) 三幕式情节架构 ===\n" f"{plot_arch_result}\n" ) arch_file = os.path.join(filepath, "Novel_architecture.txt") clear_file_content(arch_file) save_string_to_txt(final_content, arch_file) logging.info("Novel_architecture.txt has been generated successfully.") 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).")