Files
life-echo/api/app/features/memoir/chapter_markdown_compose.py
Kevin 1374f6e8f5 feat(memoir+conversation): 章节/故事最小可读字数;会话 hasUserMessage 与 UI 优化
- 后端:300 字门槛统一物化、hydrate、列表/PDF/详情;过短章节对读者隐藏
- 对话:首包前打字动画、大字模式排版、朗读/TTS 交互与布局稳定
- 首页:复用无用户消息会话;空列表「继续对话」与文案 i18n
- 章节阅读:标题进正文、封面与去重标题;阅读 Markdown 字号上调
2026-03-26 16:28:33 +08:00

73 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
按 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))