File size: 4,456 Bytes
f0b5a65 c29fb5e f0b5a65 ffb5352 f0b5a65 4e06845 f0b5a65 4e06845 f0b5a65 24a79a8 4e06845 f0b5a65 ffb5352 f0b5a65 4e06845 c29fb5e ffb5352 4e06845 f0b5a65 ffb5352 f0b5a65 ffb5352 f0b5a65 c29fb5e ffb5352 c29fb5e f0b5a65 4e06845 f0b5a65 4e06845 f0b5a65 4e06845 f0b5a65 4e06845 24a79a8 4e06845 f0b5a65 c29fb5e ffb5352 c29fb5e ffb5352 f0b5a65 ffb5352 f0b5a65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | """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",
# Voice flips move the agent by design, so the reorder-quality check
# (written for slot moves) does not apply to them.
"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
# Clause splits keep most words; require a real boundary change instead
# of the stricter paraphrase divergence gate.
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,
)
# Voice flips restructure the whole clause, so they rank above slot moves.
_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
|