- 后端:300 字门槛统一物化、hydrate、列表/PDF/详情;过短章节对读者隐藏 - 对话:首包前打字动画、大字模式排版、朗读/TTS 交互与布局稳定 - 首页:复用无用户消息会话;空列表「继续对话」与文案 i18n - 章节阅读:标题进正文、封面与去重标题;阅读 Markdown 字号上调
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""
|
||
按 chapter_story_links 顺序将各 story 正文物化为单一 markdown(无 LLM)。
|
||
保留 story 内 asset:// 引用不变。
|
||
章节级 canonical:仅正文拼接,故事间用 ---;故事标题仅存 stories.title。
|
||
PDF 导出可单独物化「## 标题 + 正文」版本。
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
from app.features.memoir.markdown_sanitize import sanitize_story_for_chapter_compose
|
||
from app.features.memoir.reading_segment_materialize import (
|
||
story_meets_minimum_chapter_length,
|
||
)
|
||
|
||
|
||
def _gather_title_body_pairs(chapter: Any) -> list[tuple[str, str]]:
|
||
links = sorted(
|
||
list(getattr(chapter, "story_links", None) or []),
|
||
key=lambda x: getattr(x, "order_index", 0),
|
||
)
|
||
pairs: list[tuple[str, str]] = []
|
||
for link in links:
|
||
st = getattr(link, "story", None)
|
||
if st is None:
|
||
continue
|
||
title = (getattr(st, "title", None) or "").strip()
|
||
body = (getattr(st, "canonical_markdown", None) or "").strip()
|
||
pairs.append((title, body))
|
||
return pairs
|
||
|
||
|
||
def compose_ordered_stories_to_markdown(ordered: list[tuple[str, str]]) -> str:
|
||
"""
|
||
:param ordered: (story_title, canonical_markdown) 已按阅读顺序排好(title 仅用于清洗去重)
|
||
:return: 章节级 markdown;仅各故事正文,非空块之间用 \\n\\n---\\n\\n 分隔
|
||
"""
|
||
bodies: list[str] = []
|
||
for title, md in ordered:
|
||
raw = (md or "").strip()
|
||
if not raw:
|
||
continue
|
||
cleaned = sanitize_story_for_chapter_compose(raw, title)
|
||
if cleaned and story_meets_minimum_chapter_length(cleaned):
|
||
bodies.append(cleaned)
|
||
return "\n\n---\n\n".join(bodies)
|
||
|
||
|
||
def compose_ordered_stories_to_pdf_markdown(ordered: list[tuple[str, str]]) -> str:
|
||
"""PDF:每故事 ## 标题 + 正文,块间 ---(标题来自元数据,不写回章节 canonical)。"""
|
||
parts: list[str] = []
|
||
for title, md in ordered:
|
||
t = (title or "").strip() or "故事"
|
||
raw = (md or "").strip()
|
||
if not raw:
|
||
continue
|
||
body = sanitize_story_for_chapter_compose(raw, title)
|
||
if not body:
|
||
continue
|
||
if not story_meets_minimum_chapter_length(body):
|
||
continue
|
||
parts.append(f"## {t}\n\n{body}")
|
||
return "\n\n---\n\n".join(parts)
|
||
|
||
|
||
def materialize_chapter_markdown_from_loaded_chapter(chapter: Any) -> str:
|
||
"""要求 chapter.story_links 已 eager-load,且各 link.story 可用。"""
|
||
return compose_ordered_stories_to_markdown(_gather_title_body_pairs(chapter))
|
||
|
||
|
||
def materialize_chapter_pdf_markdown_from_loaded_chapter(chapter: Any) -> str:
|
||
"""PDF 专用:含每段 ## 故事名。"""
|
||
return compose_ordered_stories_to_pdf_markdown(_gather_title_body_pairs(chapter))
|