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