fsi-anomaly / research /framing.py
FerrellSyntheticIntelligence's picture
backup all: 100 files (batch)
76b78ee verified
Raw
History Blame Contribute Delete
5.82 kB
"""Framing / language forensics (journalism suite layer 3).
Framing = selection + salience (Entman 1993). Deterministic proxies we can
measure without a model:
- passive voice (agency hidden: "was ordered" vs "X ordered")
- loaded/emotive terms (charged vocabulary)
- hedges (plausible deniability: "appears", "reportedly", "may")
- nominalization (actions turned into nouns: "the decision" hides who decided)
- agency: who performs the action in active-verb clauses
- omission: which sources NEVER mention a topic the others cover
These are heuristics (suit logic), not a trained detector. The suite flags;
the human decides.
Usage:
from research.framing import FramingAnalyzer
f = FramingAnalyzer()
f.add_doc("s1", "The memo was destroyed. Officials reportedly decided...")
f.report()
"""
import re
from collections import defaultdict
LOADED = [
"secret", "cover-up", "conspiracy", "plot", "scandal", "corrupt",
"fraud", "shocking", "outrage", "horrific", "brutal", "crisis",
"cover", "smear", "whistleblower", "leak", "collusion", "betrayal",
"liar", "hoax", "traitor", "unprecedented", "catastrophic",
]
HEDGES = [
"appears", "apparently", "reportedly", "allegedly", "seems", "seem",
"may", "might", "could", "possibly", "perhaps", "suggest", "claims to",
"is said to", "it is believed", "sources say", "not clear",
]
PASSIVE_RE = re.compile(r"\b(was|were|been|being|is|are)\s+(?:\w+ly\s+)?"
r"(\w+ed|torn|broken|hidden|destroyed|taken|given|"
r"made|held|filed)\b", re.IGNORECASE)
NOMINAL = re.compile(r"\b\w+(?:tion|sion|ment|ness|ity|ence|ance)\b", re.IGNORECASE)
ACTIVE_VERBS = ("said", "announced", "ordered", "admitted", "denied", "claimed",
"confirmed", "reported", "released", "disclosed", "wrote",
"testified", "warned", "decided", "approved")
_ACTIVE_RE = re.compile(r"\b([A-Z][a-zA-Z]{2,30}(?:\s+[A-Z][a-zA-Z]{2,30}){0,2})"
r"\s+(?:" + "|".join(ACTIVE_VERBS) + r")\b")
class FramingAnalyzer:
def __init__(self):
self.docs = {} # source_id -> text
def add_doc(self, source_id, text):
self.docs[source_id] = text
@staticmethod
def passive_ratio(text):
clauses = len(re.findall(r"[.!?]", text)) + 1
hits = len(PASSIVE_RE.findall(text))
return round(hits / max(clauses, 1), 3), hits
@staticmethod
def loaded_terms(text):
low = text.lower()
return [(w, low.count(w)) for w in LOADED if w in low]
@staticmethod
def hedges(text):
low = text.lower()
return [(w, low.count(w)) for w in HEDGES if w in low]
@staticmethod
def nominalizations(text):
out = defaultdict(int)
for m in NOMINAL.finditer(text):
w = m.group(0).lower()
if len(w) > 6:
out[w] += 1
return sorted(out.items(), key=lambda kv: -kv[1])[:12]
@staticmethod
def agency(text):
"""Who performs actions: leading noun phrases before active verbs."""
return [m.group(1) for m in _ACTIVE_RE.finditer(text)][:10]
def omissions(self, topics):
"""Sources that never mention a topic other sources cover."""
flags = []
for topic in topics:
low_t = topic.lower()
mentioned = [sid for sid, t in self.docs.items() if low_t in t.lower()]
if 1 <= len(mentioned) < len(self.docs):
for sid, t in self.docs.items():
if low_t not in t.lower():
flags.append({
"topic": topic,
"source_id": sid,
"flag": f"source {sid} never mentions '{topic}' "
f"while {len(mentioned)} source(s) do",
})
return flags
def doc_card(self, source_id):
text = self.docs.get(source_id, "")
if not text:
return None
ratio, passive = self.passive_ratio(text)
return {
"source_id": source_id,
"passive_ratio": ratio,
"passive_hits": passive,
"loaded": self.loaded_terms(text),
"hedges": self.hedges(text),
"nominalizations": self.nominalizations(text),
"agency": self.agency(text),
}
def report(self, topics=()):
lines = ["# Framing / Language Forensics", ""]
for sid in self.docs:
c = self.doc_card(sid)
if not c:
continue
lines.append(f"## {sid}")
lines.append(f"- passive ratio: {c['passive_ratio']} "
f"({c['passive_hits']} hits) — agency hidden where?")
if c["loaded"]:
lines.append("- loaded terms: " + ", ".join(
f"{w} x{n}" for w, n in c["loaded"]))
if c["hedges"]:
lines.append("- hedges: " + ", ".join(
f"{w} x{n}" for w, n in c["hedges"]))
if c["nominalizations"]:
lines.append("- nominalizations: " + ", ".join(
f"{w} x{n}" for w, n in c["nominalizations"][:6]))
if c["agency"]:
lines.append("- agency: " + ", ".join(c["agency"][:6]))
else:
lines.append("- agency: none found (fully passive?)")
lines.append("")
if topics:
lines.append("## Omissions (what a source does NOT say)")
for o in self.omissions(topics):
lines.append(f"- {o['flag']}")
if not self.omissions(topics):
lines.append("- all sources mention all topics, or only one source exists")
return "\n".join(lines)