any2human / app /engine /fallback /__init__.py
idnameraj's picture
Re-enable primary T5 paraphrase for stronger out1→out2 divergence.
24a79a8
Raw
History Blame Contribute Delete
4.46 kB
"""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