121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
"""Bind German 3rd-person pronouns to the last named person. Local; no new Detect tokens."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
PLACEHOLDER = re.compile(r"\[\[(PERSON:[^\]]+)\]\]")
|
|
PIECE = re.compile(r"\[\[PERSON:[^\]]+\]\]|[A-Za-zÄÖÜäöüß]+")
|
|
|
|
FEM_CUES = {
|
|
"frau", "partnerin", "tochter", "mutter", "schwester", "freundin",
|
|
"oma", "tante",
|
|
}
|
|
MASC_CUES = {
|
|
"herr", "mann", "partner", "sohn", "vater", "bruder", "freund",
|
|
"opa", "onkel",
|
|
}
|
|
FEM_PRON = {"sie", "ihr", "ihre", "ihrem", "ihren", "ihrer", "ihres"}
|
|
MASC_PRON = {"er", "ihn", "ihm", "seine", "seinem", "seinen", "seiner", "seines"}
|
|
IHR_ADDRESS = {
|
|
"seid", "habt", "wart", "könnt", "müsst", "sollt", "wollt", "dürft",
|
|
"hättet", "wäret", "werdet",
|
|
}
|
|
PLURAL_VERB = {
|
|
"sind", "waren", "haben", "hatten", "werden", "wurden",
|
|
"können", "müssen", "wollen", "sollen",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class BindState:
|
|
last: str | None = None
|
|
last_f: str | None = None
|
|
last_m: str | None = None
|
|
|
|
|
|
def _gender_from_prev(pieces: list[re.Match], index: int) -> str | None:
|
|
seen: list[str] = []
|
|
pos = index - 1
|
|
while pos >= 0 and len(seen) < 3:
|
|
raw = pieces[pos].group(0)
|
|
if PLACEHOLDER.fullmatch(raw):
|
|
break
|
|
seen.append(raw.lower())
|
|
pos -= 1
|
|
window = set(seen)
|
|
if window & FEM_CUES:
|
|
return "f"
|
|
if window & MASC_CUES:
|
|
return "m"
|
|
return None
|
|
|
|
|
|
def _referent(state: BindState, kind: str) -> str | None:
|
|
if kind == "f" and state.last_f:
|
|
return state.last_f
|
|
if kind == "m" and state.last_m:
|
|
return state.last_m
|
|
return state.last
|
|
|
|
|
|
def _is_address_ihr(prev: str, nxt: str) -> bool:
|
|
if nxt in IHR_ADDRESS or prev in IHR_ADDRESS:
|
|
return True
|
|
if prev.endswith("t") and len(prev) >= 4:
|
|
return True
|
|
return False
|
|
|
|
|
|
def bind_pronouns(text: str, state: BindState | None = None) -> tuple[str, BindState]:
|
|
state = state or BindState()
|
|
pieces = list(PIECE.finditer(text or ""))
|
|
replacements: list[tuple[int, int, str]] = []
|
|
for i, match in enumerate(pieces):
|
|
raw = match.group(0)
|
|
person = PLACEHOLDER.fullmatch(raw)
|
|
if person:
|
|
token = person.group(1)
|
|
state.last = token
|
|
gender = _gender_from_prev(pieces, i)
|
|
if gender == "f":
|
|
state.last_f = token
|
|
elif gender == "m":
|
|
state.last_m = token
|
|
continue
|
|
low = raw.lower()
|
|
prev = pieces[i - 1].group(0).lower() if i else ""
|
|
nxt = pieces[i + 1].group(0).lower() if i + 1 < len(pieces) else ""
|
|
if low == "ihr" and _is_address_ihr(prev, nxt):
|
|
continue
|
|
if low == "sie" and nxt in PLURAL_VERB:
|
|
continue
|
|
kind = "f" if low in FEM_PRON else "m" if low in MASC_PRON else ""
|
|
if not kind:
|
|
continue
|
|
ref = _referent(state, kind)
|
|
if not ref:
|
|
continue
|
|
replacements.append((match.start(), match.end(), f"[[{ref}]]"))
|
|
if not replacements:
|
|
return text or "", state
|
|
out = text or ""
|
|
for start, end, repl in reversed(replacements):
|
|
out = out[:start] + repl + out[end:]
|
|
return out, state
|
|
|
|
|
|
def bind_user_lines(text: str) -> str:
|
|
"""Only user: lines; state carries across those lines. Instructions stay untouched."""
|
|
state = BindState()
|
|
chunks: list[str] = []
|
|
for line in (text or "").splitlines(keepends=True):
|
|
ended = line.endswith("\n")
|
|
body = line[:-1] if ended else line
|
|
if body.startswith("user:"):
|
|
bound, state = bind_pronouns(body[5:], state)
|
|
chunks.append("user:" + bound + ("\n" if ended else ""))
|
|
else:
|
|
chunks.append(line)
|
|
return "".join(chunks)
|