A hybrid NER (Named Entity Recognition) approach is used for extracting PII entities from email signatures across multiple languages and writing systems.
pip install -r requirements.txt
gliner>=0.2.25 - GLiNER NER models (CVE-resolved)transformers>=4.51.3,<5.2.0 - Model backbone (CVE-resolved)email_reply_parser>=0.5.12 - Email parsingpytest>=7.0.0 - Testing frameworkurllib3>=2.2.1 - Secure HTTP (CVE-resolved)protobuf>=4.25.3 - Serialization (CVE-resolved)pillow>=10.3.0 - Imaging (CVE-resolved)pip>=24.0 - Package manager (CVE-resolved)filelock>=3.13.1 - File locking (CVE-resolved)wheel>=0.43.0 - Packaging (CVE-resolved)GLiNER_Multi_V2/
├── src/
│ ├── hybrid_approach.py # Main hybrid extraction (GLiNER + NuNER + regex)
│ └── email_sig.py # Email signature detection and parsing
├── evals/
│ ├── eval_email_parts.py # Evaluate body/signature separation
│ ├── eval_email_sig.py # Evaluate entity extraction
│ └── eval_hybrid_on_signatures.py # Combined evaluation pipeline
├── tests/
│ └── test_signatures.py # Multilingual signature extraction tests
├── data/
│ ├── email_parts_evaluation_dataset.json
│ ├── multilingual_email_evaluation_dataset.json
│ └── predicted_output/ # Evaluation results
├── requirements.txt
└── README.md
from src.hybrid_approach import extract_signature_entities
signature = """
Best regards,
Sarah Jenkins
Senior Software Engineer
CloudScale Inc.
Tel: +1-555-0199
[email protected]
"""
entities = extract_signature_entities(signature)
for ent in entities:
print(f"{ent['label'].upper()}: {ent['text']}")
Output:
PERSON: Sarah Jenkins
JOB TITLE: Senior Software Engineer
ORGANIZATION: CloudScale Inc.
PHONE NUMBER: +1-555-0199
EMAIL: [email protected]
from src.email_sig import identify_email_parts
email_text = """
Hi John,
Please find the report attached.
Best regards,
Maria López
Product Manager
"""
parts = identify_email_parts(email_text)
print("Body:", parts['body'])
print("Signature:", parts['signature_block'])
| Model | Purpose | Strengths |
|---|---|---|
urchade/gliner_multi-v2.1 | Generalist multilingual NER | High recall across scripts |
numind/NuNER_Zero-4k | Precision-focused token classification | Better accuracy for ambiguous cases |
| Regex patterns | Structured entity extraction | Reliable for emails, URLs, phones |
You might notice that we load NuNER using GLiNER.from_pretrained("numind/NuNER_Zero-4k"). This works because GLiNER is not a single fixed NER model — it's a framework and architecture that can load different pretrained checkpoints built on the same underlying design.
How it works:
Shared Architecture: Both GLiNER and NuNER use a span-based NER approach where entity labels are embedded alongside the input text, allowing zero-shot entity extraction without retraining.
Compatible Checkpoints: NuNER Zero was trained using the GLiNER architecture but with different training data and objectives (focused on higher precision). The model weights are stored in the same format, making them interchangeable.
Framework Flexibility: The GLiNER class acts as a loader that can instantiate any compatible checkpoint from HuggingFace Hub, similar to how transformers.AutoModel works.
from gliner import GLiNER
# Both use the same GLiNER class, different checkpoints
gliner_model = GLiNER.from_pretrained("urchade/gliner_multi-v2.1") # Recall-focused
nuner_model = GLiNER.from_pretrained("numind/NuNER_Zero-4k") # Precision-focused
This design allows us to ensemble multiple models with complementary strengths — GLiNER for broad multilingual coverage and NuNER for high-confidence predictions.
Run the test suite:
pytest tests/test_signatures.py -v
Test coverage includes:
The evals/ folder contains three evaluation scripts that measure different aspects of the pipeline.
| Script | Purpose | Input Dataset |
|---|---|---|
eval_email_parts.py | Body/signature separation accuracy | email_parts_evaluation_dataset.json |
eval_email_sig.py | Entity extraction precision/recall | multilingual_email_evaluation_dataset.json |
eval_hybrid_on_signatures.py | Full end-to-end pipeline | email_parts_evaluation_dataset.json |
# 1. Evaluate email body/signature separation
python evals/eval_email_parts.py
# 2. Evaluate entity extraction quality
python evals/eval_email_sig.py
# 3. Full pipeline: detect signature → extract entities
python evals/eval_hybrid_on_signatures.py
eval_email_parts.py - Signature Detection:
eval_email_sig.py - Entity Extraction:
eval_hybrid_on_signatures.py - Pipeline Stats:
Results are saved to data/predicted_output/:
predicted_output/
├── email_parts_evaluation_report.json # Detailed separation metrics
├── email_parts_evaluation_summary.csv # Aggregated stats
├── email_parts_evaluation_details.csv # Per-email results
├── email_signature_evaluation_report.json # Entity extraction metrics
├── email_signature_evaluation_summary.csv # Precision/recall by type
├── email_signature_evaluation_details.csv # Per-entity matches
├── hybrid_extraction_on_signatures.json # Full pipeline results
└── hybrid_extraction_on_signatures.csv # Entity counts per email
| Metric | Score |
|---|---|
| Overall Accuracy | 96.7% |
| Signature Detection | 100% |
| Avg Body Similarity | 97.5% |
| Avg Signature Similarity | 98.9% |
By Writing Script:
| Script | Accuracy | Body Similarity | Signature Similarity |
|---|---|---|---|
| Latin | 100% | 100% | 100% |
| Cyrillic | 100% | 100% | 100% |
| Devanagari | 100% | 100% | 100% |
| Chinese (HANS) | 90% | 92.6% | 96.5% |
| Chinese (ZHO) | 90% | 92.6% | 96.6% |
Overall Metrics (50 emails):
| Metric | Score |
|---|---|
| Precision | 77.5% |
| Recall | 73.6% |
| F1 Score | 75.5% |
| True Positives | 237 |
| Partial Matches | 64 |
By Writing Script:
| Script | Precision | Recall | F1 Score |
|---|---|---|---|
| Chinese (Zho) | 98.0% | 78.1% | 87.0% |
| Chinese (Hans) | 90.6% | 82.8% | 86.5% |
| Cyrillic | 77.6% | 85.2% | 81.3% |
| Latin | 65.9% | 84.8% | 74.2% |
| Devanagari | 62.0% | 42.5% | 50.4% |
| Metric | Value |
|---|---|
| Emails with entities | 55/60 (91.7%) |
| Total entities extracted | 165 |
| Avg entities per email | 2.75 |
Entity Type Distribution:
| Entity Type | Count |
|---|---|
| Person | 51 |
| 49 | |
| Phone Number | 24 |
| Organization | 21 |
| Job Title | 15 |
| Address | 4 |
| URL | 1 |
Adjust extraction thresholds in hybrid_approach.py:
extract_signature_entities(
text,
gliner_threshold=0.4, # Lower = more recall
nuner_threshold=0.5, # Higher = more precision
min_length=2 # Minimum entity length
)
A hybrid NER (Named Entity Recognition) approach is used for extracting PII entities from email signatures across multiple languages and writing systems.
pip install -r requirements.txt
gliner>=0.2.25 - GLiNER NER models (CVE-resolved)transformers>=4.51.3,<5.2.0 - Model backbone (CVE-resolved)email_reply_parser>=0.5.12 - Email parsingpytest>=7.0.0 - Testing frameworkurllib3>=2.2.1 - Secure HTTP (CVE-resolved)protobuf>=4.25.3 - Serialization (CVE-resolved)pillow>=10.3.0 - Imaging (CVE-resolved)pip>=24.0 - Package manager (CVE-resolved)filelock>=3.13.1 - File locking (CVE-resolved)wheel>=0.43.0 - Packaging (CVE-resolved)GLiNER_Multi_V2/
├── src/
│ ├── hybrid_approach.py # Main hybrid extraction (GLiNER + NuNER + regex)
│ └── email_sig.py # Email signature detection and parsing
├── evals/
│ ├── eval_email_parts.py # Evaluate body/signature separation
│ ├── eval_email_sig.py # Evaluate entity extraction
│ └── eval_hybrid_on_signatures.py # Combined evaluation pipeline
├── tests/
│ └── test_signatures.py # Multilingual signature extraction tests
├── data/
│ ├── email_parts_evaluation_dataset.json
│ ├── multilingual_email_evaluation_dataset.json
│ └── predicted_output/ # Evaluation results
├── requirements.txt
└── README.md
from src.hybrid_approach import extract_signature_entities
signature = """
Best regards,
Sarah Jenkins
Senior Software Engineer
CloudScale Inc.
Tel: +1-555-0199
[email protected]
"""
entities = extract_signature_entities(signature)
for ent in entities:
print(f"{ent['label'].upper()}: {ent['text']}")
Output:
PERSON: Sarah Jenkins
JOB TITLE: Senior Software Engineer
ORGANIZATION: CloudScale Inc.
PHONE NUMBER: +1-555-0199
EMAIL: [email protected]
from src.email_sig import identify_email_parts
email_text = """
Hi John,
Please find the report attached.
Best regards,
Maria López
Product Manager
"""
parts = identify_email_parts(email_text)
print("Body:", parts['body'])
print("Signature:", parts['signature_block'])
| Model | Purpose | Strengths |
|---|---|---|
urchade/gliner_multi-v2.1 | Generalist multilingual NER | High recall across scripts |
numind/NuNER_Zero-4k | Precision-focused token classification | Better accuracy for ambiguous cases |
| Regex patterns | Structured entity extraction | Reliable for emails, URLs, phones |
You might notice that we load NuNER using GLiNER.from_pretrained("numind/NuNER_Zero-4k"). This works because GLiNER is not a single fixed NER model — it's a framework and architecture that can load different pretrained checkpoints built on the same underlying design.
How it works:
Shared Architecture: Both GLiNER and NuNER use a span-based NER approach where entity labels are embedded alongside the input text, allowing zero-shot entity extraction without retraining.
Compatible Checkpoints: NuNER Zero was trained using the GLiNER architecture but with different training data and objectives (focused on higher precision). The model weights are stored in the same format, making them interchangeable.
Framework Flexibility: The GLiNER class acts as a loader that can instantiate any compatible checkpoint from HuggingFace Hub, similar to how transformers.AutoModel works.
from gliner import GLiNER
# Both use the same GLiNER class, different checkpoints
gliner_model = GLiNER.from_pretrained("urchade/gliner_multi-v2.1") # Recall-focused
nuner_model = GLiNER.from_pretrained("numind/NuNER_Zero-4k") # Precision-focused
This design allows us to ensemble multiple models with complementary strengths — GLiNER for broad multilingual coverage and NuNER for high-confidence predictions.
Run the test suite:
pytest tests/test_signatures.py -v
Test coverage includes:
The evals/ folder contains three evaluation scripts that measure different aspects of the pipeline.
| Script | Purpose | Input Dataset |
|---|---|---|
eval_email_parts.py | Body/signature separation accuracy | email_parts_evaluation_dataset.json |
eval_email_sig.py | Entity extraction precision/recall | multilingual_email_evaluation_dataset.json |
eval_hybrid_on_signatures.py | Full end-to-end pipeline | email_parts_evaluation_dataset.json |
# 1. Evaluate email body/signature separation
python evals/eval_email_parts.py
# 2. Evaluate entity extraction quality
python evals/eval_email_sig.py
# 3. Full pipeline: detect signature → extract entities
python evals/eval_hybrid_on_signatures.py
eval_email_parts.py - Signature Detection:
eval_email_sig.py - Entity Extraction:
eval_hybrid_on_signatures.py - Pipeline Stats:
Results are saved to data/predicted_output/:
predicted_output/
├── email_parts_evaluation_report.json # Detailed separation metrics
├── email_parts_evaluation_summary.csv # Aggregated stats
├── email_parts_evaluation_details.csv # Per-email results
├── email_signature_evaluation_report.json # Entity extraction metrics
├── email_signature_evaluation_summary.csv # Precision/recall by type
├── email_signature_evaluation_details.csv # Per-entity matches
├── hybrid_extraction_on_signatures.json # Full pipeline results
└── hybrid_extraction_on_signatures.csv # Entity counts per email
| Metric | Score |
|---|---|
| Overall Accuracy | 96.7% |
| Signature Detection | 100% |
| Avg Body Similarity | 97.5% |
| Avg Signature Similarity | 98.9% |
By Writing Script:
| Script | Accuracy | Body Similarity | Signature Similarity |
|---|---|---|---|
| Latin | 100% | 100% | 100% |
| Cyrillic | 100% | 100% | 100% |
| Devanagari | 100% | 100% | 100% |
| Chinese (HANS) | 90% | 92.6% | 96.5% |
| Chinese (ZHO) | 90% | 92.6% | 96.6% |
Overall Metrics (50 emails):
| Metric | Score |
|---|---|
| Precision | 77.5% |
| Recall | 73.6% |
| F1 Score | 75.5% |
| True Positives | 237 |
| Partial Matches | 64 |
By Writing Script:
| Script | Precision | Recall | F1 Score |
|---|---|---|---|
| Chinese (Zho) | 98.0% | 78.1% | 87.0% |
| Chinese (Hans) | 90.6% | 82.8% | 86.5% |
| Cyrillic | 77.6% | 85.2% | 81.3% |
| Latin | 65.9% | 84.8% | 74.2% |
| Devanagari | 62.0% | 42.5% | 50.4% |
| Metric | Value |
|---|---|
| Emails with entities | 55/60 (91.7%) |
| Total entities extracted | 165 |
| Avg entities per email | 2.75 |
Entity Type Distribution:
| Entity Type | Count |
|---|---|
| Person | 51 |
| 49 | |
| Phone Number | 24 |
| Organization | 21 |
| Job Title | 15 |
| Address | 4 |
| URL | 1 |
Adjust extraction thresholds in hybrid_approach.py:
extract_signature_entities(
text,
gliner_threshold=0.4, # Lower = more recall
nuner_threshold=0.5, # Higher = more precision
min_length=2 # Minimum entity length
)