Spaces:
Sleeping
Sleeping
| """Split and filter coaching narrative text for the web UI.""" | |
| from __future__ import annotations | |
| from chess_tutor.cohort.opening_families import bucket_display_name | |
| from chess_tutor.web.sections import ( | |
| NarrativeBlockId, | |
| narrative_block_skipped, | |
| section_enabled, | |
| WebSectionId, | |
| ) | |
| _OPENING_BUCKET_TITLES = frozenset( | |
| bucket_display_name(bucket) | |
| for bucket in ("white", "black_vs_e4", "black_vs_d4", "black_vs_c4", "other") | |
| ) | {"Other"} | |
| _WEB_NARRATIVE_SKIP_TITLES = frozenset( | |
| {"Opening patterns", "Top opening positions", "Puzzle practice"} | |
| ) | |
| _TOP_LEVEL_NARRATIVE_HEADERS = frozenset( | |
| { | |
| "Story for this player", | |
| "Your focus this week", | |
| "This week's checklist", | |
| "Examples from your games", | |
| "Opening patterns", | |
| "Top opening positions", | |
| "Practice plan", | |
| "Puzzle practice", | |
| } | |
| ) | |
| def _is_top_level_narrative_header(line: str, stripped: str) -> bool: | |
| if not stripped or line.startswith((" ", "\t")) or stripped.startswith("-"): | |
| return False | |
| return stripped.rstrip(":") in _TOP_LEVEL_NARRATIVE_HEADERS | |
| def _strip_web_duplicate_sections(narrative: str) -> str: | |
| """Remove opening/puzzle blocks when dedicated UI cards show them.""" | |
| skip_titles: set[str] = set() | |
| if section_enabled(WebSectionId.OPENINGS) and narrative_block_skipped( | |
| NarrativeBlockId.OPENING_DUPLICATE | |
| ): | |
| skip_titles.add("Opening patterns") | |
| skip_titles.add("Top opening positions") | |
| if section_enabled(WebSectionId.PUZZLES) and narrative_block_skipped( | |
| NarrativeBlockId.PUZZLE_DUPLICATE | |
| ): | |
| skip_titles.add("Puzzle practice") | |
| if not skip_titles: | |
| return narrative.strip() | |
| lines = narrative.splitlines() | |
| result: list[str] = [] | |
| skipping = False | |
| i = 0 | |
| while i < len(lines): | |
| line = lines[i] | |
| stripped = line.strip() | |
| if stripped in skip_titles or stripped.rstrip(":") in skip_titles: | |
| skipping = True | |
| i += 1 | |
| continue | |
| if skipping: | |
| if _is_top_level_narrative_header(line, stripped): | |
| skipping = False | |
| else: | |
| i += 1 | |
| continue | |
| result.append(line) | |
| i += 1 | |
| return "\n".join(result).strip() | |
| def _looks_like_opening_section(section: dict[str, str]) -> bool: | |
| title = section["title"] | |
| body = section["body"] | |
| if title in _WEB_NARRATIVE_SKIP_TITLES or title in _OPENING_BUCKET_TITLES: | |
| return True | |
| if title.startswith("#") and " — " in title: | |
| return True | |
| return "You play:" in body and "Recommended:" in body | |
| def _looks_like_puzzle_section(section: dict[str, str]) -> bool: | |
| title = section["title"] | |
| body = section["body"] | |
| if title in _WEB_NARRATIVE_SKIP_TITLES: | |
| return True | |
| if "Rated ladder" in title or "Rated ladder" in body: | |
| return True | |
| if "lichess.org/training/" in body: | |
| return True | |
| if "Themes:" in body and " Elo (" in body: | |
| return True | |
| return False | |
| def _looks_like_coaching_report_section(section: dict[str, str]) -> bool: | |
| title = section["title"].strip() | |
| if title in {"Coaching report", "Story for this player"}: | |
| return True | |
| return section["body"].strip().startswith("Story for this player") | |
| def _looks_like_cohort_tendency_section(section: dict[str, str]) -> bool: | |
| title = section["title"].strip().rstrip(":") | |
| return title.startswith("Compared to others at your rating") | |
| _NARRATIVE_BLOCK_MATCHERS: dict[NarrativeBlockId, object] = { | |
| NarrativeBlockId.COACHING_REPORT: _looks_like_coaching_report_section, | |
| NarrativeBlockId.COHORT_TENDENCY: _looks_like_cohort_tendency_section, | |
| NarrativeBlockId.OPENING_DUPLICATE: _looks_like_opening_section, | |
| NarrativeBlockId.PUZZLE_DUPLICATE: _looks_like_puzzle_section, | |
| } | |
| _CLIMBER_SECTION_PREFIX = "Players who climbed from your bracket toward" | |
| def _web_friendly_narrative_title(title: str) -> str: | |
| stripped = title.strip().rstrip(":") | |
| if stripped.startswith(_CLIMBER_SECTION_PREFIX): | |
| return "Playstyle changes (from similar climbers)" | |
| if stripped == "Practical emphasis (by priority)": | |
| return "Recommended playstyle changes" | |
| return title | |
| def _should_skip_narrative_section( | |
| section: dict[str, str], | |
| *, | |
| filters: tuple | None = None, | |
| ) -> bool: | |
| for block_id, matcher in _NARRATIVE_BLOCK_MATCHERS.items(): | |
| if narrative_block_skipped(block_id, filters=filters) and matcher(section): | |
| return True | |
| return False | |
| def _split_narrative_sections(narrative: str) -> list[dict[str, str]]: | |
| """Split coaching narrative into titled sections.""" | |
| text = narrative.strip() | |
| if not text: | |
| return [] | |
| sections: list[dict[str, str]] = [] | |
| current_title = "Coaching report" | |
| current_lines: list[str] = [] | |
| for line in text.splitlines(): | |
| stripped = line.strip() | |
| if not stripped and not current_lines: | |
| continue | |
| if ( | |
| stripped | |
| and not stripped.startswith("-") | |
| and not line.startswith(" ") | |
| and not line.startswith("\t") | |
| and stripped.endswith(":") | |
| and len(stripped) < 80 | |
| ): | |
| if current_lines: | |
| sections.append( | |
| { | |
| "title": current_title, | |
| "body": "\n".join(current_lines).strip(), | |
| } | |
| ) | |
| current_title = stripped.rstrip(":") | |
| current_lines = [] | |
| continue | |
| current_lines.append(line.rstrip()) | |
| if current_lines: | |
| sections.append( | |
| { | |
| "title": current_title, | |
| "body": "\n".join(current_lines).strip(), | |
| } | |
| ) | |
| return sections | |
| def build_web_narrative_sections( | |
| narrative: str, | |
| *, | |
| narrative_filters: tuple | None = None, | |
| ) -> list[dict[str, str]]: | |
| """Narrative blocks for the web UI, respecting section registry filters.""" | |
| if not section_enabled(WebSectionId.NARRATIVE): | |
| return [] | |
| cleaned = _strip_web_duplicate_sections(narrative) | |
| sections: list[dict[str, str]] = [] | |
| for section in _split_narrative_sections(cleaned): | |
| if _should_skip_narrative_section(section, filters=narrative_filters): | |
| continue | |
| sections.append( | |
| { | |
| "title": _web_friendly_narrative_title(section["title"]), | |
| "body": section["body"], | |
| } | |
| ) | |
| return sections | |