Bilingual NLP tool for comparing how news outlets frame the same event through agency, wording, and coverage differences.
4
stars
29
commits
Python
primary language
Aug 30, 2026
updated
Structural comparison of how news outlets frame the same event.
news-deframe is a bilingual NLP tool for examining how news coverage differs across outlets. Rather than assigning bias scores or deciding which source is more objective, it surfaces observable structural differences: who is positioned as an agent or patient, which claims are shared or absent, how entities are described, and which articles exhibit similar framing patterns.
It supports two complementary workflows:
diff — detailed pairwise comparison between two articles.analyze — event-level analysis across multiple articles.Traditional Chinese, Simplified Chinese, and English are supported. Language is detected automatically from article text; no --lang option is required.
news-deframe exposes framing evidence; humans interpret it.
Framing is contextual. A structural difference alone does not establish bias, intent, accuracy, or truthfulness.
news-deframe therefore reports observations such as:
It deliberately does not automatically conclude that:
The output is evidence for human interpretation, not a verdict.
| Feature | diff | analyze | Description |
|---|---|---|---|
| Bilingual NLP | ✓ | ✓ | Automatic Chinese/English detection with language-specific spaCy pipelines |
| SVO Extraction | ✓ | ✓ | Extracts subject–verb–object relationships and active/passive constructions |
| Entity Framing | ✓ | ✓ | Examines named entities, modifiers, and structural roles |
| Semantic Alignment | ✓ | — | Aligns semantically similar sentences between two articles |
| Unshared Claims | ✓ | — | Surfaces claims found in only one side of a pairwise comparison |
| Claim Coverage | — | ✓ | Integrated table of shared/single-outlet claims and absent outlets |
| Actor Framing Blocks | — | ✓ | Compares entity agency/patient patterns and associated actions by outlet |
| Framing Clusters | — | ✓ | Groups articles with similar structural framing profiles |
| Presentation Tiers | — | ✓ | Concise default summary, --details for coding, --verbose for diagnostics |
| Rich Terminal Reports | ✓ | ✓ | Terminal-width-aware, human-readable colour reports |
| JSON Export | ✓ | ✓ | Structured Pydantic v2 output for research and downstream processing |
The following models are downloaded automatically on first use — no manual setup required:
| Model | Purpose |
|---|---|
zh_core_web_trf | Chinese NLP pipeline (spaCy) |
en_core_web_trf | English NLP pipeline (spaCy) |
paraphrase-multilingual-MiniLM-L12-v2 | Multilingual semantic embeddings (sentence-transformers) |
Only the model needed for the language being analysed is loaded. If Chinese-only articles are used, the English model is never downloaded.
Clone the repository and install:
git clone https://github.com/422August/news-deframe.git
cd news-deframe
pip install -e .
That's it. On first run, any missing language models are downloaded automatically:
Downloading language model 'zh_core_web_trf'...
Loading multilingual semantic embedding model 'paraphrase-multilingual-MiniLM-L12-v2'...
For development (includes pytest):
pip install -e ".[dev]"
Use analyze when comparing several articles covering the same event.
The recommended workflow is to place the articles for one event in a directory:
articles/
└── event_001/
├── outlet_a.txt
├── outlet_b.txt
├── outlet_c.txt
└── outlet_d.txt
Then run:
news-deframe analyze articles/event_001/
The directory name becomes the default event ID, while each filename stem becomes its article/outlet ID.
For example:
articles/election_debate/
├── reuters.txt
├── outlet_a.txt
└── outlet_b.txt
produces the event ID election_debate and article IDs reuters, outlet_a, and outlet_b.
Directories outside the repository work as well:
news-deframe analyze "/path/to/event_articles/"
Multiple files may also be supplied explicitly:
news-deframe analyze a.txt b.txt c.txt
Event directories are loaded non-recursively.
.txt files are read as UTF-8.One directory should contain articles covering the same event. Mixing unrelated events will reduce the usefulness of claim clustering and framing comparisons.
news-deframe analyze provides three presentation tiers tailored for different analytical needs:
--details: Detailed claim-level evidence, present/absent outlets, and full source sentences for human coding and manual inspection.--verbose (-v): Technical diagnostics including cluster centroid feature values, sentence similarity scores, exact ratio denominators, and all actors.--format json: Full structured Pydantic v2 data model export for downstream statistical processing and archival.news-deframe analyze articles/event_001/
Displays:
Shared by all outlets, Shared by majority, Single-outlet claims).--details)news-deframe analyze articles/event_001/ --details
Adds claim-level panels containing:
--verbose or -v)news-deframe analyze articles/event_001/ --verbose
Exposes engineering diagnostics:
passive_ratio, mean_agent_ratio, mean_patient_ratio, etc.)role_occurrence_count = agent_count + patient_count)Passive Pt) and evaluative modifiersnews-deframe analyze articles/event_001/ --details --verbose
Shows full source sentences with similarity scores (sim=0.76) along with centroid feature values and complete diagnostics.
Adjust semantic similarity threshold:
news-deframe analyze articles/event_001/ --threshold 0.55
Set the requested number of framing clusters:
news-deframe analyze articles/event_001/ --clusters 3
Export structured JSON:
news-deframe analyze articles/event_001/ --format json
Save JSON to file:
news-deframe analyze articles/event_001/ \
--format json \
--output event_report.json
Use diff for a focused comparison between exactly two articles:
news-deframe diff article_a.txt article_b.txt
The pairwise report includes sentence alignment, unshared claims, passive-voice statistics, and entity framing information.
Adjust the semantic matching threshold:
news-deframe diff article_a.txt article_b.txt --threshold 0.5
Export the result as JSON:
news-deframe diff article_a.txt article_b.txt --format json
Or write it directly to a file:
news-deframe diff article_a.txt article_b.txt \
--format json \
--output diff_report.json
The original diff workflow remains independent from event-level analysis. analyze extends the project to multi-document corpora rather than replacing pairwise comparison.
Given a collection of articles describing the same event, news-deframe analyze builds an event-level representation:
Event
│
├── Articles
│
├── Integrated Claim Coverage
│ ├── summary counts (all / majority / single-outlet)
│ ├── descriptive coverage categories
│ ├── absent outlets
│ └── representative claims
│
├── Actor Framing by Outlet
│ ├── agent ratios
│ ├── patient ratios
│ ├── role observations (agent + patient occurrences)
│ └── associated actions / predicates
│
├── Framing Clusters
│ ├── structural profile membership
│ └── cluster centroids (in --verbose)
│
└── Research Interpretation Notes
Sentences across all articles are embedded and verified for propositional equivalence. Related claims are grouped into clusters and classified by descriptive coverage frequency:
Shared by all outlets: 3
Shared by majority: 1
Single-outlet claims: 20
Claim Coverage Category Missing Representative
C01 3/3 Widely shared — Police and organizers confirmed...
C02 3/3 Widely shared — According to police, demonstrators...
C04 2/3 Commonly reported outlet_a An officer sustained minor injuries...
C05 1/3 Single-outlet b, c A rally took place in downtown square...
Coverage measures reporting frequency across the corpus. It does not establish truth, and absence does not imply intentional omission.
Rather than combining disparate metrics into cramped matrix cells, actor framing is presented in clean vertical blocks that separate grammatical positioning from lexical actions:
Actor: Police
Outlet Agent Patient Role observations
a 1.00 0.00 13
b 0.85 0.15 13
c 0.93 0.07 14
Associated actions:
a: arrest, state, note, demand
b: arrest, require, state, obstruct
c: require, investigate, arrest
agent / (agent + patient)).Articles are grouped by unsupervised structural feature similarity (syntactic voice, agency/patient distributions, entity density, and claim participation).
Framing Cluster 1, Framing Cluster 2).--verbose or --format json.Every report concludes with concise methodology reminders:
All public analysis structures use Pydantic v2 models.
class EventAnalysis(BaseModel):
event_id: str
articles: list[ParsedArticle]
article_ids: list[str]
claim_clusters: list[ClaimCluster]
entity_outlet_matrix: EntityOutletMatrix
framing_clusters: list[FramingCluster]
consensus_view: ConsensusView
class DiffReport(BaseModel):
article_a_id: str
article_b_id: str
alignments: list[SentenceAlignment]
unshared_claims_a: list[str]
unshared_claims_b: list[str]
passive_ratio_a: float
passive_ratio_b: float
JSON export exposes these structures for visualization, statistical analysis, archival use, or integration with other tools.
news-deframe/
├── articles/
│ └── .gitkeep
├── tests/
│ ├── fixtures/
│ ├── test_svo.py
│ ├── test_entities.py
│ ├── test_diff.py
│ ├── test_article_loader.py
│ ├── test_analysis.py
│ ├── test_cli_analyze.py
│ ├── test_event_console_formatter.py
│ └── test_corpora.py
├── src/
│ └── news_deframe/
│ ├── __init__.py
│ ├── cli.py
│ ├── schemas.py
│ ├── parser/
│ │ ├── spacy_loader.py
│ │ ├── svo.py
│ │ ├── entities.py
│ │ └── article_loader.py
│ ├── diff/
│ │ ├── aligner.py
│ │ └── coverage.py
│ ├── analysis/
│ │ ├── claims.py
│ │ ├── entity_matrix.py
│ │ ├── framing_clusters.py
│ │ ├── consensus.py
│ │ ├── event.py
│ │ └── schemas.py
│ └── formatters/
│ ├── console.py
│ ├── event_console.py
│ └── json_export.py
├── pyproject.toml
└── README.md
The articles/ directory is a conventional local workspace. Article corpora may be stored anywhere on the filesystem and passed to news-deframe analyze.
The parsing and analysis layers are modular and can be used independently of the CLI.
Pairwise parsing continues to use the existing parser:
from news_deframe.parser.spacy_loader import get_nlp
from news_deframe.parser.svo import extract_svo
from news_deframe.parser.entities import extract_entity_modifiers
from news_deframe.diff.coverage import compute_coverage
from news_deframe.schemas import ParsedArticle
def parse(text: str, article_id: str) -> ParsedArticle:
nlp = get_nlp(text)
doc = nlp(text)
return ParsedArticle(
article_id=article_id,
sentences=[
sentence.text.strip()
for sentence in doc.sents
if sentence.text.strip()
],
svo_records=extract_svo(doc),
entity_modifiers=extract_entity_modifiers(doc),
)
Event-level components are separated into dedicated analysis modules so that claim clustering, entity matrices, consensus analysis, and framing clustering can evolve independently.
The test suite is designed to run without downloading NLP or embedding models. External model calls are replaced with deterministic test doubles where appropriate.
Run all tests:
pytest
Generate a coverage report:
pytest --cov=news_deframe --cov-report=term-missing
Tests cover both the original pairwise workflow and event-level functionality, including folder loading, multi-document claim clustering, entity matrices, consensus analysis, framing clustering, Chinese/English corpora, JSON serialization, and backward compatibility.
Human interpretation first. Structural measurements are evidence, not judgments about bias, intent, or truth.
Additive workflows. analyze extends news-deframe; it does not replace the focused diff workflow.
No import side effects. NLP and embedding models are loaded lazily rather than during package import.
Automatic language detection. Chinese and English articles are routed to the appropriate NLP pipeline without a language flag.
Modular analysis. Parsing, pairwise diffing, event analysis, and presentation remain separate concerns.
Strict schemas. Public data structures use Pydantic v2 models.
Testability. NLP and embedding layers can be mocked so the test suite remains fast and offline-safe.
Machine-readable results. Console output is designed for humans, while JSON output preserves structured evidence for downstream analysis.
news-deframe performs computational linguistic analysis and should not be treated as an automated fact-checker or political-bias classifier.
Semantic similarity does not guarantee that two sentences make exactly the same claim. Entity recognition and grammatical-role extraction can also produce errors, particularly with ambiguous language or complex sentence structures.
Results should therefore be treated as analytical signals to inspect, with the original articles remaining the authoritative context for interpretation.
29 commits
Python
100.0%
Bilingual NLP tool for comparing how news outlets frame the same event through agency, wording, and coverage differences.
4
stars
29
commits
Python
primary language
Aug 30, 2026
updated
Structural comparison of how news outlets frame the same event.
news-deframe is a bilingual NLP tool for examining how news coverage differs across outlets. Rather than assigning bias scores or deciding which source is more objective, it surfaces observable structural differences: who is positioned as an agent or patient, which claims are shared or absent, how entities are described, and which articles exhibit similar framing patterns.
It supports two complementary workflows:
diff — detailed pairwise comparison between two articles.analyze — event-level analysis across multiple articles.Traditional Chinese, Simplified Chinese, and English are supported. Language is detected automatically from article text; no --lang option is required.
news-deframe exposes framing evidence; humans interpret it.
Framing is contextual. A structural difference alone does not establish bias, intent, accuracy, or truthfulness.
news-deframe therefore reports observations such as:
It deliberately does not automatically conclude that:
The output is evidence for human interpretation, not a verdict.
| Feature | diff | analyze | Description |
|---|---|---|---|
| Bilingual NLP | ✓ | ✓ | Automatic Chinese/English detection with language-specific spaCy pipelines |
| SVO Extraction | ✓ | ✓ | Extracts subject–verb–object relationships and active/passive constructions |
| Entity Framing | ✓ | ✓ | Examines named entities, modifiers, and structural roles |
| Semantic Alignment | ✓ | — | Aligns semantically similar sentences between two articles |
| Unshared Claims | ✓ | — | Surfaces claims found in only one side of a pairwise comparison |
| Claim Coverage | — | ✓ | Integrated table of shared/single-outlet claims and absent outlets |
| Actor Framing Blocks | — | ✓ | Compares entity agency/patient patterns and associated actions by outlet |
| Framing Clusters | — | ✓ | Groups articles with similar structural framing profiles |
| Presentation Tiers | — | ✓ | Concise default summary, --details for coding, --verbose for diagnostics |
| Rich Terminal Reports | ✓ | ✓ | Terminal-width-aware, human-readable colour reports |
| JSON Export | ✓ | ✓ | Structured Pydantic v2 output for research and downstream processing |
The following models are downloaded automatically on first use — no manual setup required:
| Model | Purpose |
|---|---|
zh_core_web_trf | Chinese NLP pipeline (spaCy) |
en_core_web_trf | English NLP pipeline (spaCy) |
paraphrase-multilingual-MiniLM-L12-v2 | Multilingual semantic embeddings (sentence-transformers) |
Only the model needed for the language being analysed is loaded. If Chinese-only articles are used, the English model is never downloaded.
Clone the repository and install:
git clone https://github.com/422August/news-deframe.git
cd news-deframe
pip install -e .
That's it. On first run, any missing language models are downloaded automatically:
Downloading language model 'zh_core_web_trf'...
Loading multilingual semantic embedding model 'paraphrase-multilingual-MiniLM-L12-v2'...
For development (includes pytest):
pip install -e ".[dev]"
Use analyze when comparing several articles covering the same event.
The recommended workflow is to place the articles for one event in a directory:
articles/
└── event_001/
├── outlet_a.txt
├── outlet_b.txt
├── outlet_c.txt
└── outlet_d.txt
Then run:
news-deframe analyze articles/event_001/
The directory name becomes the default event ID, while each filename stem becomes its article/outlet ID.
For example:
articles/election_debate/
├── reuters.txt
├── outlet_a.txt
└── outlet_b.txt
produces the event ID election_debate and article IDs reuters, outlet_a, and outlet_b.
Directories outside the repository work as well:
news-deframe analyze "/path/to/event_articles/"
Multiple files may also be supplied explicitly:
news-deframe analyze a.txt b.txt c.txt
Event directories are loaded non-recursively.
.txt files are read as UTF-8.One directory should contain articles covering the same event. Mixing unrelated events will reduce the usefulness of claim clustering and framing comparisons.
news-deframe analyze provides three presentation tiers tailored for different analytical needs:
--details: Detailed claim-level evidence, present/absent outlets, and full source sentences for human coding and manual inspection.--verbose (-v): Technical diagnostics including cluster centroid feature values, sentence similarity scores, exact ratio denominators, and all actors.--format json: Full structured Pydantic v2 data model export for downstream statistical processing and archival.news-deframe analyze articles/event_001/
Displays:
Shared by all outlets, Shared by majority, Single-outlet claims).--details)news-deframe analyze articles/event_001/ --details
Adds claim-level panels containing:
--verbose or -v)news-deframe analyze articles/event_001/ --verbose
Exposes engineering diagnostics:
passive_ratio, mean_agent_ratio, mean_patient_ratio, etc.)role_occurrence_count = agent_count + patient_count)Passive Pt) and evaluative modifiersnews-deframe analyze articles/event_001/ --details --verbose
Shows full source sentences with similarity scores (sim=0.76) along with centroid feature values and complete diagnostics.
Adjust semantic similarity threshold:
news-deframe analyze articles/event_001/ --threshold 0.55
Set the requested number of framing clusters:
news-deframe analyze articles/event_001/ --clusters 3
Export structured JSON:
news-deframe analyze articles/event_001/ --format json
Save JSON to file:
news-deframe analyze articles/event_001/ \
--format json \
--output event_report.json
Use diff for a focused comparison between exactly two articles:
news-deframe diff article_a.txt article_b.txt
The pairwise report includes sentence alignment, unshared claims, passive-voice statistics, and entity framing information.
Adjust the semantic matching threshold:
news-deframe diff article_a.txt article_b.txt --threshold 0.5
Export the result as JSON:
news-deframe diff article_a.txt article_b.txt --format json
Or write it directly to a file:
news-deframe diff article_a.txt article_b.txt \
--format json \
--output diff_report.json
The original diff workflow remains independent from event-level analysis. analyze extends the project to multi-document corpora rather than replacing pairwise comparison.
Given a collection of articles describing the same event, news-deframe analyze builds an event-level representation:
Event
│
├── Articles
│
├── Integrated Claim Coverage
│ ├── summary counts (all / majority / single-outlet)
│ ├── descriptive coverage categories
│ ├── absent outlets
│ └── representative claims
│
├── Actor Framing by Outlet
│ ├── agent ratios
│ ├── patient ratios
│ ├── role observations (agent + patient occurrences)
│ └── associated actions / predicates
│
├── Framing Clusters
│ ├── structural profile membership
│ └── cluster centroids (in --verbose)
│
└── Research Interpretation Notes
Sentences across all articles are embedded and verified for propositional equivalence. Related claims are grouped into clusters and classified by descriptive coverage frequency:
Shared by all outlets: 3
Shared by majority: 1
Single-outlet claims: 20
Claim Coverage Category Missing Representative
C01 3/3 Widely shared — Police and organizers confirmed...
C02 3/3 Widely shared — According to police, demonstrators...
C04 2/3 Commonly reported outlet_a An officer sustained minor injuries...
C05 1/3 Single-outlet b, c A rally took place in downtown square...
Coverage measures reporting frequency across the corpus. It does not establish truth, and absence does not imply intentional omission.
Rather than combining disparate metrics into cramped matrix cells, actor framing is presented in clean vertical blocks that separate grammatical positioning from lexical actions:
Actor: Police
Outlet Agent Patient Role observations
a 1.00 0.00 13
b 0.85 0.15 13
c 0.93 0.07 14
Associated actions:
a: arrest, state, note, demand
b: arrest, require, state, obstruct
c: require, investigate, arrest
agent / (agent + patient)).Articles are grouped by unsupervised structural feature similarity (syntactic voice, agency/patient distributions, entity density, and claim participation).
Framing Cluster 1, Framing Cluster 2).--verbose or --format json.Every report concludes with concise methodology reminders:
All public analysis structures use Pydantic v2 models.
class EventAnalysis(BaseModel):
event_id: str
articles: list[ParsedArticle]
article_ids: list[str]
claim_clusters: list[ClaimCluster]
entity_outlet_matrix: EntityOutletMatrix
framing_clusters: list[FramingCluster]
consensus_view: ConsensusView
class DiffReport(BaseModel):
article_a_id: str
article_b_id: str
alignments: list[SentenceAlignment]
unshared_claims_a: list[str]
unshared_claims_b: list[str]
passive_ratio_a: float
passive_ratio_b: float
JSON export exposes these structures for visualization, statistical analysis, archival use, or integration with other tools.
news-deframe/
├── articles/
│ └── .gitkeep
├── tests/
│ ├── fixtures/
│ ├── test_svo.py
│ ├── test_entities.py
│ ├── test_diff.py
│ ├── test_article_loader.py
│ ├── test_analysis.py
│ ├── test_cli_analyze.py
│ ├── test_event_console_formatter.py
│ └── test_corpora.py
├── src/
│ └── news_deframe/
│ ├── __init__.py
│ ├── cli.py
│ ├── schemas.py
│ ├── parser/
│ │ ├── spacy_loader.py
│ │ ├── svo.py
│ │ ├── entities.py
│ │ └── article_loader.py
│ ├── diff/
│ │ ├── aligner.py
│ │ └── coverage.py
│ ├── analysis/
│ │ ├── claims.py
│ │ ├── entity_matrix.py
│ │ ├── framing_clusters.py
│ │ ├── consensus.py
│ │ ├── event.py
│ │ └── schemas.py
│ └── formatters/
│ ├── console.py
│ ├── event_console.py
│ └── json_export.py
├── pyproject.toml
└── README.md
The articles/ directory is a conventional local workspace. Article corpora may be stored anywhere on the filesystem and passed to news-deframe analyze.
The parsing and analysis layers are modular and can be used independently of the CLI.
Pairwise parsing continues to use the existing parser:
from news_deframe.parser.spacy_loader import get_nlp
from news_deframe.parser.svo import extract_svo
from news_deframe.parser.entities import extract_entity_modifiers
from news_deframe.diff.coverage import compute_coverage
from news_deframe.schemas import ParsedArticle
def parse(text: str, article_id: str) -> ParsedArticle:
nlp = get_nlp(text)
doc = nlp(text)
return ParsedArticle(
article_id=article_id,
sentences=[
sentence.text.strip()
for sentence in doc.sents
if sentence.text.strip()
],
svo_records=extract_svo(doc),
entity_modifiers=extract_entity_modifiers(doc),
)
Event-level components are separated into dedicated analysis modules so that claim clustering, entity matrices, consensus analysis, and framing clustering can evolve independently.
The test suite is designed to run without downloading NLP or embedding models. External model calls are replaced with deterministic test doubles where appropriate.
Run all tests:
pytest
Generate a coverage report:
pytest --cov=news_deframe --cov-report=term-missing
Tests cover both the original pairwise workflow and event-level functionality, including folder loading, multi-document claim clustering, entity matrices, consensus analysis, framing clustering, Chinese/English corpora, JSON serialization, and backward compatibility.
Human interpretation first. Structural measurements are evidence, not judgments about bias, intent, or truth.
Additive workflows. analyze extends news-deframe; it does not replace the focused diff workflow.
No import side effects. NLP and embedding models are loaded lazily rather than during package import.
Automatic language detection. Chinese and English articles are routed to the appropriate NLP pipeline without a language flag.
Modular analysis. Parsing, pairwise diffing, event analysis, and presentation remain separate concerns.
Strict schemas. Public data structures use Pydantic v2 models.
Testability. NLP and embedding layers can be mocked so the test suite remains fast and offline-safe.
Machine-readable results. Console output is designed for humans, while JSON output preserves structured evidence for downstream analysis.
news-deframe performs computational linguistic analysis and should not be treated as an automated fact-checker or political-bias classifier.
Semantic similarity does not guarantee that two sentences make exactly the same claim. Entity recognition and grammatical-role extraction can also produce errors, particularly with ambiguous language or complex sentence structures.
Results should therefore be treated as analytical signals to inspect, with the original articles remaining the authoritative context for interpretation.
29 commits
Python
100.0%