174 lines
6.5 KiB
Python
174 lines
6.5 KiB
Python
"""
|
||
目标页数与一级篇章数量区间:阈值与 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} 字同量级,末级子目不宜过细'
|
||
)
|