""" 目标页数与一级篇章数量区间:阈值与 generator._effective_volume 一致。 小章节(自动填充子目录行)总条数:与「目标页数」线性映射,见 subchapter_total_* 与 allocate_subchapters_to_main *。 """ from __future__ import annotations import random from typing import List, Optional, Tuple # 与 modules.generator._effective_volume 页数分界一致 PAGE_VOLUME_THRESHOLDS = (125, 175, 225) # 各篇幅档位对应的一级篇章数量 [min, max](与页数映射表一致) TOP_LEVEL_CHAPTER_RANGES = { 'concise': (6, 8), 'standard': (8, 10), 'detailed': (10, 12), 'full': (12, 16), } # 小章节总条数 = slope * pages + intercept(过点 100->78, 300->212) SUBCHAPTER_PAGES_SLOPE = 0.67 SUBCHAPTER_PAGES_INTERCEPT = 11.0 SUBCHAPTER_JITTER_LOW = 0.9 SUBCHAPTER_JITTER_HIGH = 1.1 # expand 在请求/库/配置均未给出页数时,按 100 页 ≈ 基线 78 章 ±10%,避免小章节失控到数百 EXPAND_OUTLINE_DEFAULT_TARGET_PAGES = 100 def subchapter_total_base_from_pages(pages: int) -> float: return SUBCHAPTER_PAGES_SLOPE * float(pages) + SUBCHAPTER_PAGES_INTERCEPT def subchapter_jitter_bounds(n_base: float) -> Tuple[int, int]: """ 对线性基线 N_base 的严格 ±10% 整数闭区间 [lo, hi](用于全标小章节行总数抽样后夹紧)。 例:N_base=78(约 100 页)→ lo=70, hi=86。 """ lo = max(1, int(round(n_base * SUBCHAPTER_JITTER_LOW))) hi = max(lo, int(round(n_base * SUBCHAPTER_JITTER_HIGH))) return lo, hi def subchapter_total_effective( pages: int, k: int, rng: Optional[random.Random] = None, ) -> int: """ 在目标页数 P 下,对一次「小章节自动填充」抽样的子章节行总数上界(全标合计)。 先按 N_base(P)=0.67*P+11 与 U~Uniform(0.9,1.1) 取整,再**严格夹紧**到 [round(N_base*0.9), round(N_base*1.1)], 故 100 页时锚定 78±10% → 恒在 70–86 条(在仅受随机影响时)。 不再用 max(n, k) 抬升总数:主章数 k 很大时若强行「每章至少 1 条」会把 N 抬到 300+,与 78±10% 目标冲突。 当 n < k 时由 allocate_subchapters_to_mains 将额度优先分给部分主章,其余主章 quota 为 0(该次不填小章)。 pages<=0 或 k<=0 时返回 0(调用方不应在 TARGET_PAGES>0 且可扩展主章>0 之外使用)。 """ if pages <= 0 or k <= 0: return 0 r = rng if rng is not None else random.Random() n_base = subchapter_total_base_from_pages(pages) lo, hi = subchapter_jitter_bounds(n_base) n = int(round(n_base * r.uniform(SUBCHAPTER_JITTER_LOW, SUBCHAPTER_JITTER_HIGH))) n = min(max(n, lo), hi) return n def allocate_subchapters_to_mains(n: int, k: int) -> List[int]: """ 将整数 n 均分到 k 个主章:前 n%k 个主章得 floor+1,其余得 floor;k=0 返回 []。 """ if k <= 0: return [] n = max(0, n) q, r = n // k, n % k return [q + 1] * r + [q] * (k - r) def resolve_expand_target_pages( request_pages: Optional[int], no_subchapter_limit: bool, db_pages: int, config_pages: int, ) -> int: """ 得到本次「自动填充小章节」使用的目标页数 P(>0 则启用条数上界,0=不限制)。 显式不限制时返回 0;否则优先正数 request → 落库值 → 全局配置 → 默认 100 页。 """ if no_subchapter_limit: return 0 if request_pages is not None and int(request_pages) > 0: return int(request_pages) d = int(db_pages or 0) if d > 0: return d c = int(config_pages or 0) if c > 0: return c return EXPAND_OUTLINE_DEFAULT_TARGET_PAGES def volume_key_from_target_pages(pages: int, content_volume_default: str = 'standard') -> str: """与 _effective_volume 相同逻辑的档位 key(不读 config,便于测试)。""" if pages <= 0: return content_volume_default if pages <= PAGE_VOLUME_THRESHOLDS[0]: return 'concise' if pages <= PAGE_VOLUME_THRESHOLDS[1]: return 'standard' if pages <= PAGE_VOLUME_THRESHOLDS[2]: return 'detailed' return 'full' def top_level_chapter_range_from_pages(pages: int, content_volume_default: str = 'standard') -> Tuple[int, int]: """ 返回一级篇章数量区间 (lo, hi)。 未设置目标页数时沿用默认 8–10 章。 """ if pages <= 0: return TOP_LEVEL_CHAPTER_RANGES['standard'] vk = volume_key_from_target_pages(pages, content_volume_default) return TOP_LEVEL_CHAPTER_RANGES[vk] def outline_chapter_count_hint( pages: int, content_volume_default: str = 'standard', page_char_estimate: int = 700, ) -> str: """ 嵌入大纲提示词的篇章约束句(替换原固定「8–10 个」相关描述)。 当 pages>0 时提醒:全稿正文字量与「页数×每页字数」可替换的总目标同量级,目录 层次不宜过细,以免成稿后每节可写篇幅过薄、难成合理技术应答。 """ pce = max(1, int(page_char_estimate or 700)) if pages <= 0: return ( '总的章节数应该控制在8-10个,一级篇章总数不超过10个' ) lo, hi = top_level_chapter_range_from_pages(pages, content_volume_default) total_g = int(round(pages * pce)) return ( f'总的章节数应该控制在约 {lo}–{hi} 个,一级篇章总数不超过 {hi} 个' f'(目标约 {pages} 页,按目标页数映射的篇幅档位估算)。' f'全稿正文字量规模需与总目标约 {total_g} 字' f'({pages} 页×约每页 {pce} 字的粗略换算计)同量级,目录层次与末级小节目不宜过细,' f'避免叶节数过多时单节篇幅过薄、难以成文。' ) def outline_chapter_count_hint_with_rating_variant( pages: int, content_volume_default: str = 'standard', page_char_estimate: int = 700, ) -> str: """带评分目录模板中的同类约束(原含「不超过10个」的收紧表述)。""" pce = max(1, int(page_char_estimate or 700)) if pages <= 0: return ( '总的章节数应该控制在8-10个,不超过10个' ) lo, hi = top_level_chapter_range_from_pages(pages, content_volume_default) total_g = int(round(pages * pce)) return ( f'总的章节数应该控制在约 {lo}–{hi} 个,不超过{hi} 个' f'(目标约 {pages} 页,按目标页数映射的篇幅档位估算)' f'全稿正文字量约与总目标 {total_g} 字同量级,末级子目不宜过细' )