File: //opt/textanalyse/tools/rhetorical_structure_tool.py
# tools/rhetorical_structure_tool.py
try:
import spacy
except Exception as e:
print(f"Warning: spaCy import disabled in rhetorical_structure_tool: {e}")
spacy = None
from typing import Any, Dict, List, Tuple
# Lade spaCy‑Modell für Deutsch
if spacy:
try:
nlp = spacy.load("de_core_news_sm")
except Exception as e:
print(f"Warning: could not load spaCy model: {e}")
nlp = None
else:
nlp = None
# -----------------------------------------------------------
# Hilfsfunktionen
# -----------------------------------------------------------
def _segment_edus(doc: "spacy.tokens.Doc") -> List[str]:
"""
Schneidet einen spaCy‑Doc in Elementary Discourse Units (EDUs).
Heuristiken:
• Satzende
• Komma vor/ nach Subjunktion oder Relativpronomen
• Gedankenstrich, Semikolon, Doppelpunkt
"""
edus: List[str] = []
current_tokens: List[str] = []
def _flush():
txt = " ".join(current_tokens).strip()
if txt:
edus.append(txt)
current_tokens.clear()
for sent in doc.sents:
for tok in sent:
current_tokens.append(tok.text)
# starker Cut
if tok.is_sent_end or tok.text in {";", ":", "–", "—"}:
_flush()
# weicher Cut bei Kommata + Subjunktion/Relativ
elif tok.text == ",":
if tok.i + 1 < len(doc):
nxt = doc[tok.i + 1]
if nxt.dep_ in {"mark", "advmod", "relcl"} or nxt.pos_ == "SCONJ":
_flush()
_flush()
return edus
def _classify_relation(head_edu: str, dep_edu: str) -> str:
"""
Grobe Klassifikation der RST‑Relation zwischen zwei EDUs
(nur für Demo‑Zwecke, keine linguistische Vollständigkeit).
"""
causal_markers = {"weil", "da", "denn", "zumal"}
contrast_markers = {"aber", "jedoch", "trotzdem", "doch"}
cond_markers = {"wenn", "falls", "sofern"}
dep_first = dep_edu.split()[0].lower()
if dep_first in causal_markers:
return "Cause"
if dep_first in contrast_markers:
return "Contrast"
if dep_first in cond_markers:
return "Condition"
if dep_first.endswith("lich") or dep_first == "daher":
return "Result"
# Default
return "Elaboration"
def _build_binary_tree(edus: List[str]) -> Dict[str, Any]:
"""
Baut rekursiv einen binären RST‑Baum (rechtsverzweigend).
Die Wurzel ist die erste EDU (Nucleus); jede nachfolgende
EDU wird als Satellite angehängt.
"""
if len(edus) == 1:
return {"text": edus[0], "relation": "span", "children": []}
nucleus, satellite = edus[0], edus[1]
relation = _classify_relation(nucleus, satellite)
node = {
"text": nucleus,
"relation": relation,
"children": [
{"text": satellite, "relation": "satellite", "children": []},
_build_binary_tree(edus[1:]) # rechtsrekursiv
],
}
return node
# -----------------------------------------------------------
# Öffentliches API
# -----------------------------------------------------------
def parse_rhetorical_structure(text: str) -> Dict[str, Any]:
# Stub when spaCy disabled
if nlp is None:
return {"error": "Rhetorical structure tool disabled due to spaCy incompatibility."}
doc = nlp(text)
edus = _segment_edus(doc)
if not edus:
return {"text": text.strip(), "relation": "span", "children": []}
return _build_binary_tree(edus)