| """Safer varied structural fallbacks when paraphrase cannot change wording."""
|
|
|
| from __future__ import annotations
|
|
|
| from app.config import ENGINE_SPLIT_LONG, ENGINE_SPLIT_MIN_WORDS
|
| from app.engine.paraphrase import sufficiently_changed
|
| from app.engine.plan import build_plan
|
| from app.engine.rewrite import generate_candidates, reorder_quality_ok
|
| from app.engine.split import try_split_coordinated_verbs, try_split_long_sentence
|
| from app.engine.templates import (
|
| try_because_front,
|
| try_by_agent_front,
|
| try_complex_clause_swap,
|
| try_copula_np_invert,
|
| try_for_purpose_front,
|
| try_in_both_front,
|
| try_in_pp_front,
|
| try_such_as_front,
|
| try_unfront_opener,
|
| try_when_clause_front,
|
| )
|
| from app.engine.voice import active_to_passive, passive_to_active
|
|
|
| _DEDICATED = frozenset(
|
| {
|
| "for_purpose_front",
|
| "in_pp_front",
|
| "copula_np_invert",
|
| "such_as_front",
|
| "when_clause_front",
|
| "by_agent_front",
|
| "in_both_front",
|
| "split_long",
|
| "split_coordinated_verbs",
|
|
|
|
|
| "passive_to_active",
|
| "active_to_passive",
|
| }
|
| )
|
|
|
|
|
| def structural_fallback_candidates(
|
| text: str,
|
| *,
|
| min_confidence: float = 0.35,
|
| include_plan: bool = True,
|
| ) -> list[tuple[str, str, float]]:
|
| """Return (template_id, rewritten, confidence) reorder options.
|
|
|
| Prefers voice/clause moves over fixed clefts. Never invents new wording.
|
| Set ``include_plan`` false when the caller already holds the plan's slot
|
| templates; rebuilding them here costs a second parse for no new options.
|
| """
|
| source = (text or "").strip()
|
| if not source:
|
| return []
|
|
|
| results: list[tuple[str, str, float]] = []
|
| seen: set[str] = set()
|
|
|
| def _add(template_id: str, candidate: str | None, confidence: float) -> None:
|
| if not candidate:
|
| return
|
| cleaned = candidate.strip()
|
| key = cleaned.lower().rstrip(".!?")
|
| if not cleaned or key in seen:
|
| return
|
| if key == source.lower().rstrip(".!?"):
|
| return
|
|
|
|
|
| if template_id in {"split_long", "split_coordinated_verbs"}:
|
| src_stops = sum(source.count(mark) for mark in ".!?")
|
| cand_stops = sum(cleaned.count(mark) for mark in ".!?")
|
| if cand_stops <= src_stops:
|
| return
|
| elif not sufficiently_changed(source, cleaned):
|
| return
|
| if template_id not in _DEDICATED and not reorder_quality_ok(source, cleaned):
|
| return
|
| seen.add(key)
|
| results.append((template_id, cleaned, confidence))
|
|
|
| _add("when_clause_front", try_when_clause_front(source), 0.80)
|
| _add("because_front", try_because_front(source), 0.78)
|
| _add("for_purpose_front", try_for_purpose_front(source), 0.76)
|
| _add("in_both_front", try_in_both_front(source), 0.75)
|
| _add("in_pp_front", try_in_pp_front(source), 0.74)
|
| _add("complex_clause_swap", try_complex_clause_swap(source), 0.74)
|
| _add("unfront_opener", try_unfront_opener(source), 0.77)
|
| _add("by_agent_front", try_by_agent_front(source), 0.73)
|
| _add("copula_np_invert", try_copula_np_invert(source), 0.72)
|
| _add("such_as_front", try_such_as_front(source), 0.70)
|
| if ENGINE_SPLIT_LONG:
|
| _add(
|
| "split_coordinated_verbs",
|
| try_split_coordinated_verbs(source, min_words=ENGINE_SPLIT_MIN_WORDS),
|
| 0.71,
|
| )
|
| _add(
|
| "split_long",
|
| try_split_long_sentence(source, min_words=ENGINE_SPLIT_MIN_WORDS),
|
| 0.69,
|
| )
|
|
|
| _add("passive_to_active", passive_to_active(source), 0.79)
|
| _add("active_to_passive", active_to_passive(source), 0.68)
|
|
|
| if include_plan:
|
| plan = build_plan(source, min_confidence=min_confidence)
|
| if plan.safe:
|
| for template_id, candidate, confidence in generate_candidates(plan):
|
| _add(template_id, candidate, confidence)
|
|
|
| results.sort(key=lambda item: -item[2])
|
| return results
|
|
|