| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Runtime validators/normalizers for state.json sections.
- """
- from __future__ import annotations
- import re
- from typing import Any, Dict, List, Mapping, Optional, Sequence
- FORESHADOWING_STATUS_PENDING = "未回收"
- FORESHADOWING_STATUS_RESOLVED = "已回收"
- FORESHADOWING_TIER_CORE = "核心"
- FORESHADOWING_TIER_SUB = "支线"
- FORESHADOWING_TIER_DECOR = "装饰"
- FORESHADOWING_PLANTED_KEYS = [
- "planted_chapter",
- "added_chapter",
- "source_chapter",
- "start_chapter",
- "chapter",
- ]
- FORESHADOWING_TARGET_KEYS = [
- "target_chapter",
- "due_chapter",
- "deadline_chapter",
- "resolve_by_chapter",
- "target",
- ]
- _PENDING_STATUS_TEXT = {"未回收", "待回收", "进行中", "未解决", "pending", "active"}
- _RESOLVED_STATUS_TEXT = {"已回收", "已完成", "已解决", "完成", "resolved", "done", "complete"}
- _TIER_CORE_TEXT = {"核心", "主线", "core", "main"}
- _TIER_DECOR_TEXT = {"装饰", "次要", "decor", "decoration"}
- _PATTERN_FIELDS = [
- "coolpoint_patterns",
- "coolpoint_pattern",
- "cool_point_patterns",
- "cool_point_pattern",
- "patterns",
- "pattern",
- ]
- _PATTERN_SPLIT_RE = re.compile(r"[、,,/|+;;。]+")
- def to_positive_int(value: Any) -> Optional[int]:
- if value is None or isinstance(value, bool):
- return None
- try:
- number = int(value)
- return number if number > 0 else None
- except (TypeError, ValueError):
- if isinstance(value, str):
- matched = re.search(r"\d+", value)
- if matched:
- number = int(matched.group(0))
- return number if number > 0 else None
- return None
- def resolve_chapter_field(item: Mapping[str, Any], keys: Sequence[str]) -> Optional[int]:
- for key in keys:
- if key in item:
- chapter = to_positive_int(item.get(key))
- if chapter is not None:
- return chapter
- return None
- def normalize_foreshadowing_status(
- raw_status: Any,
- default: str = FORESHADOWING_STATUS_PENDING,
- ) -> str:
- text = str(raw_status or "").strip()
- if not text:
- return default
- text_lower = text.lower()
- if (
- text in _RESOLVED_STATUS_TEXT
- or text_lower in _RESOLVED_STATUS_TEXT
- or FORESHADOWING_STATUS_RESOLVED in text
- ):
- return FORESHADOWING_STATUS_RESOLVED
- if text in _PENDING_STATUS_TEXT or text_lower in _PENDING_STATUS_TEXT:
- return FORESHADOWING_STATUS_PENDING
- return default
- def is_resolved_foreshadowing_status(raw_status: Any) -> bool:
- return normalize_foreshadowing_status(raw_status) == FORESHADOWING_STATUS_RESOLVED
- def normalize_foreshadowing_tier(
- raw_tier: Any,
- default: str = FORESHADOWING_TIER_SUB,
- ) -> str:
- text = str(raw_tier or "").strip()
- if not text:
- return default
- text_lower = text.lower()
- if text in _TIER_CORE_TEXT or text_lower in _TIER_CORE_TEXT:
- return FORESHADOWING_TIER_CORE
- if text in _TIER_DECOR_TEXT or text_lower in _TIER_DECOR_TEXT:
- return FORESHADOWING_TIER_DECOR
- return default
- def split_patterns(raw_value: Any) -> List[str]:
- if raw_value is None:
- return []
- tokens: List[str] = []
- if isinstance(raw_value, list):
- for item in raw_value:
- text = str(item).strip()
- if text:
- tokens.append(text)
- elif isinstance(raw_value, str):
- text = raw_value.strip()
- if not text:
- return []
- split_values = [part.strip() for part in _PATTERN_SPLIT_RE.split(text)]
- tokens.extend([part for part in split_values if part])
- else:
- return []
- deduped: List[str] = []
- seen = set()
- for token in tokens:
- if token not in seen:
- seen.add(token)
- deduped.append(token)
- return deduped
- def count_patterns(raw_value: Any) -> Optional[int]:
- patterns = split_patterns(raw_value)
- if not patterns:
- return None
- return len(patterns)
- def normalize_foreshadowing_item(item: Mapping[str, Any]) -> Dict[str, Any]:
- normalized = dict(item)
- normalized["status"] = normalize_foreshadowing_status(item.get("status"))
- normalized["tier"] = normalize_foreshadowing_tier(item.get("tier"))
- content = str(item.get("content") or "").strip()
- if content:
- normalized["content"] = content
- planted_chapter = resolve_chapter_field(item, FORESHADOWING_PLANTED_KEYS)
- if planted_chapter is not None:
- normalized["planted_chapter"] = planted_chapter
- target_chapter = resolve_chapter_field(item, FORESHADOWING_TARGET_KEYS)
- if target_chapter is not None:
- normalized["target_chapter"] = target_chapter
- resolved_chapter = resolve_chapter_field(item, ["resolved_chapter", "resolved_at_chapter", "resolved"])
- if resolved_chapter is not None:
- normalized["resolved_chapter"] = resolved_chapter
- return normalized
- def normalize_foreshadowing_list(raw_items: Any) -> List[Dict[str, Any]]:
- if not isinstance(raw_items, list):
- return []
- normalized: List[Dict[str, Any]] = []
- for raw_item in raw_items:
- if isinstance(raw_item, Mapping):
- normalized.append(normalize_foreshadowing_item(raw_item))
- return normalized
- def normalize_chapter_meta_entry(entry: Mapping[str, Any]) -> Dict[str, Any]:
- normalized = dict(entry)
- merged_patterns: List[str] = []
- seen = set()
- for field_name in _PATTERN_FIELDS:
- for pattern in split_patterns(entry.get(field_name)):
- if pattern not in seen:
- seen.add(pattern)
- merged_patterns.append(pattern)
- if merged_patterns:
- normalized["coolpoint_patterns"] = merged_patterns
- return normalized
- def normalize_chapter_meta(raw_chapter_meta: Any) -> Dict[str, Dict[str, Any]]:
- if not isinstance(raw_chapter_meta, Mapping):
- return {}
- normalized: Dict[str, Dict[str, Any]] = {}
- for chapter_key, chapter_entry in raw_chapter_meta.items():
- if isinstance(chapter_entry, Mapping):
- normalized[str(chapter_key)] = normalize_chapter_meta_entry(chapter_entry)
- return normalized
- def get_chapter_meta_entry(state: Mapping[str, Any], chapter: int) -> Dict[str, Any]:
- chapter_meta = state.get("chapter_meta", {})
- if not isinstance(chapter_meta, Mapping):
- return {}
- for lookup_key in (f"{chapter:04d}", str(chapter)):
- value = chapter_meta.get(lookup_key)
- if isinstance(value, Mapping):
- return normalize_chapter_meta_entry(value)
- for raw_key, raw_value in chapter_meta.items():
- if to_positive_int(raw_key) == chapter and isinstance(raw_value, Mapping):
- return normalize_chapter_meta_entry(raw_value)
- return {}
- def normalize_state_runtime_sections(state: Dict[str, Any]) -> Dict[str, Any]:
- if not isinstance(state, dict):
- return {}
- plot_threads = state.get("plot_threads")
- if not isinstance(plot_threads, dict):
- plot_threads = {}
- state["plot_threads"] = plot_threads
- plot_threads["foreshadowing"] = normalize_foreshadowing_list(plot_threads.get("foreshadowing"))
- state["chapter_meta"] = normalize_chapter_meta(state.get("chapter_meta", {}))
- return state
|