| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Writing guidance and checklist builders.
- """
- from __future__ import annotations
- from typing import Any, Dict, List
- from .genre_aliases import to_profile_key
- GENRE_GUIDANCE_TEXT: dict[str, str] = {
- "xianxia": "题材加权:强化升级/对抗结果的可见反馈,术语解释后置。",
- "shuangwen": "题材加权:维持高爽点密度,主爽点外叠加一个副轴反差。",
- "urban-power": "题材加权:优先写社会反馈链(他人反应→资源变化→地位变化)。",
- "romance": "题材加权:每章推进关系位移,避免情绪原地打转。",
- "mystery": "题材加权:线索必须可回收,优先以规则冲突制造悬念。",
- "rules-mystery": "题材加权:规则先于解释,代价先于胜利。",
- "zhihu-short": "题材加权:压缩铺垫,优先反转与高强度结尾钩。",
- "substitute": "题材加权:强化误解-拉扯-决断链路,避免重复虐点。",
- "esports": "题材加权:每场对抗至少写清一个战术决策点与其后果。",
- "livestream": "题材加权:强化“外部反馈→主角反制→数据变化”即时闭环。",
- "cosmic-horror": "题材加权:恐怖来源于规则与代价,不依赖空泛惊悚形容。",
- }
- def build_guidance_items(
- *,
- chapter: int,
- reader_signal: Dict[str, Any],
- genre_profile: Dict[str, Any],
- low_score_threshold: float,
- hook_diversify_enabled: bool,
- ) -> Dict[str, Any]:
- guidance: List[str] = []
- low_ranges = reader_signal.get("low_score_ranges") or []
- if low_ranges:
- worst = min(
- low_ranges,
- key=lambda row: float(row.get("overall_score", 9999)),
- )
- guidance.append(
- f"第{chapter}章优先修复近期低分段问题:参考{worst.get('start_chapter')}-{worst.get('end_chapter')}章,强化冲突推进与结尾钩子。"
- )
- hook_usage = reader_signal.get("hook_type_usage") or {}
- if hook_usage and hook_diversify_enabled:
- dominant_hook = max(hook_usage.items(), key=lambda kv: kv[1])[0]
- guidance.append(
- f"近期钩子类型“{dominant_hook}”使用偏多,本章建议做钩子差异化,避免连续同构。"
- )
- pattern_usage = reader_signal.get("pattern_usage") or {}
- if pattern_usage:
- top_pattern = max(pattern_usage.items(), key=lambda kv: kv[1])[0]
- guidance.append(
- f"爽点模式“{top_pattern}”近期高频,本章可保留主爽点但叠加一个新爽点副轴。"
- )
- review_trend = reader_signal.get("review_trend") or {}
- overall_avg = review_trend.get("overall_avg")
- if isinstance(overall_avg, (int, float)) and float(overall_avg) < low_score_threshold:
- guidance.append(
- f"最近审查均分{overall_avg:.1f}低于阈值{low_score_threshold:.1f},建议先保稳:减少跳场、每段补动作结果闭环。"
- )
- genre = str(genre_profile.get("genre") or "").strip()
- refs = genre_profile.get("reference_hints") or []
- if genre:
- guidance.append(f"题材锚定:按“{genre}”叙事主线推进,保持题材读者预期稳定兑现。")
- if refs:
- guidance.append(f"题材策略可执行提示:{refs[0]}")
- guidance.append("网文节奏基线:章首300字内给出目标与阻力,章末保留未闭合问题。")
- guidance.append("兑现密度基线:每600-900字给一次微兑现,并确保本章至少1处可量化变化。")
- normalized_genre = to_profile_key(genre)
- genre_hint = GENRE_GUIDANCE_TEXT.get(normalized_genre)
- if genre_hint:
- guidance.append(genre_hint)
- composite_hints = genre_profile.get("composite_hints") or []
- if composite_hints:
- guidance.append(f"复合题材协同:{composite_hints[0]}")
- if not guidance:
- guidance.append("本章执行默认高可读策略:冲突前置、信息后置、段末留钩。")
- return {
- "guidance": guidance,
- "low_ranges": low_ranges,
- "hook_usage": hook_usage,
- "pattern_usage": pattern_usage,
- "genre": genre,
- }
- def build_writing_checklist(
- *,
- guidance_items: List[str],
- reader_signal: Dict[str, Any],
- genre_profile: Dict[str, Any],
- min_items: int,
- max_items: int,
- default_weight: float,
- ) -> List[Dict[str, Any]]:
- items: List[Dict[str, Any]] = []
- def _add_item(
- item_id: str,
- label: str,
- *,
- weight: float | None = None,
- required: bool = False,
- source: str = "writing_guidance",
- verify_hint: str = "",
- ) -> None:
- if len(items) >= max_items:
- return
- if any(row.get("id") == item_id for row in items):
- return
- item_weight = float(weight if weight is not None else default_weight)
- if item_weight <= 0:
- item_weight = default_weight
- items.append(
- {
- "id": item_id,
- "label": label,
- "weight": round(item_weight, 2),
- "required": bool(required),
- "source": source,
- "verify_hint": verify_hint,
- }
- )
- low_ranges = reader_signal.get("low_score_ranges") or []
- if low_ranges:
- worst = min(low_ranges, key=lambda row: float(row.get("overall_score", 9999)))
- span = f"{worst.get('start_chapter')}-{worst.get('end_chapter')}"
- _add_item(
- "fix_low_score_range",
- f"修复低分区间问题(参考第{span}章)",
- weight=max(default_weight, 1.4),
- required=True,
- source="reader_signal.low_score_ranges",
- verify_hint="至少完成1处冲突升级,并在段末留下钩子。",
- )
- hook_usage = reader_signal.get("hook_type_usage") or {}
- if hook_usage:
- dominant_hook = max(hook_usage.items(), key=lambda kv: kv[1])[0]
- _add_item(
- "hook_diversification",
- f"钩子差异化(避免继续单一“{dominant_hook}”)",
- weight=max(default_weight, 1.2),
- required=True,
- source="reader_signal.hook_type_usage",
- verify_hint="结尾钩子类型与近20章主类型至少有一处差异。",
- )
- pattern_usage = reader_signal.get("pattern_usage") or {}
- if pattern_usage:
- top_pattern = max(pattern_usage.items(), key=lambda kv: kv[1])[0]
- _add_item(
- "coolpoint_combo",
- f"主爽点+副爽点组合(主爽点:{top_pattern})",
- weight=default_weight,
- required=False,
- source="reader_signal.pattern_usage",
- verify_hint="新增至少1个副爽点,并与主爽点形成因果链。",
- )
- review_trend = reader_signal.get("review_trend") or {}
- overall_avg = review_trend.get("overall_avg")
- if isinstance(overall_avg, (int, float)):
- _add_item(
- "readability_loop",
- "段落可读性闭环(动作→结果→情绪)",
- weight=max(default_weight, 1.1),
- required=True,
- source="reader_signal.review_trend",
- verify_hint="抽查3段,均包含动作结果闭环。",
- )
- genre = str(genre_profile.get("genre") or "").strip()
- if genre:
- _add_item(
- "genre_anchor_consistency",
- f"题材锚定一致性({genre})",
- weight=max(default_weight, 1.1),
- required=True,
- source="genre_profile.genre",
- verify_hint="主冲突与题材核心承诺保持一致。",
- )
- for idx, text in enumerate(guidance_items, start=1):
- if len(items) >= max_items:
- break
- label = str(text).strip()
- if not label:
- continue
- _add_item(
- f"guidance_item_{idx}",
- label,
- weight=default_weight,
- required=False,
- source="writing_guidance.guidance_items",
- verify_hint="完成后可在正文中定位对应段落。",
- )
- fallback_items = [
- (
- "opening_conflict",
- "开篇300字内给出冲突触发",
- "开头段出现明确目标与阻力。",
- ),
- (
- "scene_goal_block",
- "场景目标与阻力清晰",
- "每个场景至少有1个可验证目标。",
- ),
- (
- "ending_hook",
- "段末留钩并引出下一问",
- "结尾出现未解问题或下一步行动。",
- ),
- ]
- for item_id, label, verify_hint in fallback_items:
- if len(items) >= min_items or len(items) >= max_items:
- break
- _add_item(
- item_id,
- label,
- weight=default_weight,
- required=False,
- source="fallback",
- verify_hint=verify_hint,
- )
- return items[:max_items]
- def is_checklist_item_completed(item: Dict[str, Any], reader_signal: Dict[str, Any]) -> bool:
- item_id = str(item.get("id") or "")
- if item_id in {"fix_low_score_range", "readability_loop"}:
- review_trend = reader_signal.get("review_trend") or {}
- overall = review_trend.get("overall_avg")
- return isinstance(overall, (int, float)) and float(overall) >= 75.0
- if item_id == "hook_diversification":
- hook_usage = reader_signal.get("hook_type_usage") or {}
- return len(hook_usage) >= 2
- if item_id == "coolpoint_combo":
- pattern_usage = reader_signal.get("pattern_usage") or {}
- return len(pattern_usage) >= 2
- if item_id == "genre_anchor_consistency":
- return True
- source = str(item.get("source") or "")
- if source.startswith("fallback"):
- return True
- return False
|