initial: novel-app snapshot
This commit is contained in:
137
services/deconstruct.py
Normal file
137
services/deconstruct.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""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})
|
||||
Reference in New Issue
Block a user