tech-bid-manage20260423/utils/volume_chapters.py
2026-04-23 14:37:19 +08:00

174 lines
6.5 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.

"""
目标页数与一级篇章数量区间:阈值与 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% → 恒在 7086 条(在仅受随机影响时)。
不再用 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其余得 floork=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)。
未设置目标页数时沿用默认 810 章。
"""
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:
"""
嵌入大纲提示词的篇章约束句替换原固定「810 个」相关描述)。
当 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} 字同量级,末级子目不宜过细'
)