Refuse voice flips that would strand openers or preverbal adverbs.
Browse filesLeftovers from active_to_passive land after the agent, which silently moved fronted phrases and words like "also" / "carefully" into the wrong place. Hyphenated compounds (high-quality) are kept intact.
Co-authored-by: Cursor <cursoragent@cursor.com>
app/engine/voice/__init__.py
CHANGED
|
@@ -28,6 +28,17 @@ def _ranges_text(doc, indices: set[int]) -> str:
|
|
| 28 |
return " ".join(part for part in parts if part)
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def _continue_case(text: str, head) -> str:
|
| 32 |
value = text.strip()
|
| 33 |
if not value or head.pos_ == "PROPN":
|
|
@@ -221,7 +232,7 @@ def active_to_passive(text: str) -> str | None:
|
|
| 221 |
| {root.i}
|
| 222 |
| {token.i for token in auxiliaries}
|
| 223 |
| {token.i for token in negations}
|
| 224 |
-
| {token.i for token in doc if token
|
| 225 |
)
|
| 226 |
extras = {
|
| 227 |
token.i
|
|
@@ -229,6 +240,19 @@ def active_to_passive(text: str) -> str | None:
|
|
| 229 |
if token.i not in consumed
|
| 230 |
}
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
new_subject = _start_case(_ranges_text(doc, object_indices))
|
| 233 |
agent = _continue_case(_ranges_text(doc, subject_indices), subject)
|
| 234 |
complement = _ranges_text(doc, complement_indices)
|
|
|
|
| 28 |
return " ".join(part for part in parts if part)
|
| 29 |
|
| 30 |
|
| 31 |
+
def _is_structural_punct(token, doc) -> bool:
|
| 32 |
+
"""Punctuation that separates phrases, as opposed to joining a compound.
|
| 33 |
+
|
| 34 |
+
The hyphens in "high-quality" and "word-of-mouth" are ``punct`` too;
|
| 35 |
+
dropping them would rebuild the sentence as "high quality".
|
| 36 |
+
"""
|
| 37 |
+
if token.dep_ != "punct":
|
| 38 |
+
return False
|
| 39 |
+
return bool(token.whitespace_) or token.i == len(doc) - 1
|
| 40 |
+
|
| 41 |
+
|
| 42 |
def _continue_case(text: str, head) -> str:
|
| 43 |
value = text.strip()
|
| 44 |
if not value or head.pos_ == "PROPN":
|
|
|
|
| 232 |
| {root.i}
|
| 233 |
| {token.i for token in auxiliaries}
|
| 234 |
| {token.i for token in negations}
|
| 235 |
+
| {token.i for token in doc if _is_structural_punct(token, doc)}
|
| 236 |
)
|
| 237 |
extras = {
|
| 238 |
token.i
|
|
|
|
| 240 |
if token.i not in consumed
|
| 241 |
}
|
| 242 |
|
| 243 |
+
# Leftovers are appended after the agent, which only reads correctly for
|
| 244 |
+
# trailing modifiers. Anything that opened the sentence ("By delivering
|
| 245 |
+
# good service, organizations can …") or sat before the verb ("also")
|
| 246 |
+
# would land in the wrong place and change what it modifies.
|
| 247 |
+
if extras:
|
| 248 |
+
subject_start = min(subject_indices)
|
| 249 |
+
if any(index < subject_start for index in extras):
|
| 250 |
+
return None
|
| 251 |
+
if any(
|
| 252 |
+
doc[index].pos_ in {"ADV", "PART"} and index < root.i for index in extras
|
| 253 |
+
):
|
| 254 |
+
return None
|
| 255 |
+
|
| 256 |
new_subject = _start_case(_ranges_text(doc, object_indices))
|
| 257 |
agent = _continue_case(_ranges_text(doc, subject_indices), subject)
|
| 258 |
complement = _ranges_text(doc, complement_indices)
|
tests/test_structural_variation.py
CHANGED
|
@@ -37,6 +37,27 @@ def test_passive_to_active_skips_negation_needing_do_support():
|
|
| 37 |
assert passive_to_active("Mistakes were not corrected by the editor.") is None
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
def test_active_and_passive_are_not_mutually_reachable():
|
| 41 |
text = "Readers reviewed the report yesterday."
|
| 42 |
passive = active_to_passive(text)
|
|
|
|
| 37 |
assert passive_to_active("Mistakes were not corrected by the editor.") is None
|
| 38 |
|
| 39 |
|
| 40 |
+
def test_active_to_passive_refuses_to_relocate_a_fronted_phrase():
|
| 41 |
+
"""Leftovers land after the agent, which would strip the opener's scope."""
|
| 42 |
+
text = (
|
| 43 |
+
"By consistently delivering high-quality service, organizations can "
|
| 44 |
+
"establish a strong reputation."
|
| 45 |
+
)
|
| 46 |
+
assert active_to_passive(text) is None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def test_active_to_passive_refuses_to_strand_a_preverbal_adverb():
|
| 50 |
+
assert active_to_passive(
|
| 51 |
+
"Organizations can also encourage positive recommendations."
|
| 52 |
+
) is None
|
| 53 |
+
assert active_to_passive("Maria carefully submitted the final report.") is None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_active_to_passive_keeps_hyphenated_compounds_intact():
|
| 57 |
+
result = active_to_passive("The team published the high-quality version last week.")
|
| 58 |
+
assert result == "The high-quality version was published by the team last week."
|
| 59 |
+
|
| 60 |
+
|
| 61 |
def test_active_and_passive_are_not_mutually_reachable():
|
| 62 |
text = "Readers reviewed the report yesterday."
|
| 63 |
passive = active_to_passive(text)
|