"""Book deconstruction (拆书) service: novel architect that produces concept, world, characters, outline, and foreshadowing sections. Pure functions — no Flask imports. Each section is streamed via :func:`llm.make_token_stream` with ``section_start`` / ``section_end`` SSE bookends. """ import logging from services import llm logger = logging.getLogger(__name__) SECTIONS = ["concept", "world", "characters", "outline", "foreshadowing"] SYSTEM_PROMPT = ( "你是一位资深小说架构师, 精通网文结构设计与节奏把控。" "请根据用户的需求, 逐步构建完整的小说框架。" "输出纯文本, 不要使用 Markdown 标题标记 (# / ## 等), 用换行分段即可。" ) # ---- per-section Chinese user prompts ----------------------------------- _SECTION_PROMPTS = { "concept": ( "请为小说《{title}》设计核心概念, 包括:\n" "1. 主题 (这部小说想表达什么)\n" "2. 核心冲突 (主角面临的主要矛盾)\n" "3. 一句话卖点 (最能吸引读者的概括)\n" "类型: {genre} | 风格: {style} | 目标字数: {target_words}" ), "world": ( "请为小说《{title}》设计世界观设定, 包括:\n" "1. 时代背景\n" "2. 主要地点\n" "3. 力量体系 (如有)\n" "4. 社会结构\n" "类型: {genre} | 风格: {style}" ), "characters": ( "请为小说《{title}》设计主要角色表。\n" "每个角色单独一段, 以【角色】名字 开头, 包含:\n" "名字 / 定位 / 性格 / 动机 / 成长弧线。\n" "类型: {genre} | 风格: {style}" ), "outline": ( "请为小说《{title}》规划三幕结构与卷章大纲。\n" "格式: 第N卷 / 第N章 标题 — 一句话概要\n" "预估总章数按目标字数 {target_words} 除以每章 5000 字计算。\n" "类型: {genre} | 风格: {style}" ), "foreshadowing": ( "请为小说《{title}》设计伏笔计划。\n" "每条伏笔单独一段, 以【伏笔】开头, 包含:\n" "描述 / 埋设位置 (第几卷第几章) / 回收位置。\n" "类型: {genre} | 风格: {style}" ), } def _format_inputs(inputs): """Return (title, genre, style, target_words, reference_novel, confirmed).""" return ( inputs.get("title", "未命名"), inputs.get("genre", "未指定"), inputs.get("style", "未指定"), inputs.get("target_words", "未指定"), inputs.get("reference_novel"), inputs.get("confirmed", {}), ) def _confirmed_context(confirmed): """Render previously confirmed sections as context for the next prompt.""" if not confirmed: return "" parts = [] for sec in SECTIONS: text = confirmed.get(sec) if text: parts.append(f"【已确认 - {sec}】\n{text}") return "\n\n".join(parts) def build_section_prompt(section, inputs): """Build the messages list for a given section. ``inputs`` is a dict with keys: title, genre, style, target_words, reference_novel (optional), confirmed (dict of previous section texts). Returns a list of ``{"role": ..., "content": ...}`` dicts ready for the LLM chat API. """ title, genre, style, target_words, reference_novel, confirmed = _format_inputs(inputs) template = _SECTION_PROMPTS.get(section) if template is None: raise ValueError(f"unknown section: {section}") user_prompt = template.format( title=title, genre=genre, style=style, target_words=target_words, ) if reference_novel: user_prompt += f"\n\n参考小说: 《{reference_novel}》, 参考其节奏与结构但不抄袭。" ctx = _confirmed_context(confirmed) if ctx: user_prompt = f"以下是已确认的前序设定:\n\n{ctx}\n\n---\n\n{user_prompt}" return [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ] def stream_section(section, inputs): """Generator yielding SSE frames for a section. Emits ``section_start`` → token frames → ``section_end`` (with full accumulated content). Uses :func:`llm.make_token_stream` under the hood. """ messages = build_section_prompt(section, inputs) yield llm._sse("section_start", {"section": section}) parts = [] def _on_token(t): parts.append(t) yield from llm.make_token_stream(messages, _on_token, max_tokens=4096) full_text = "".join(parts) yield llm._sse("section_end", {"section": section, "content": full_text})