| import os |
| import re |
| import gradio as gr |
| from functools import lru_cache |
| from camel_tools.ner import NERecognizer |
| from camel_tools.disambig.bert import BERTUnfactoredDisambiguator |
| from camel_tools.tokenizers.word import simple_word_tokenize |
| from camel_tools.utils.normalize import normalize_unicode |
| from camel_tools.utils.dediac import dediac_ar |
|
|
| |
| data_path = os.path.expanduser("~/.camel_tools") |
| os.environ["CAMELTOOLS_DATA"] = data_path |
|
|
| |
| ner_path = os.path.join(data_path, "data", "ner", "arabert") |
| disambig_path = os.path.join(data_path, "data", "disambig_bert_unfactored", "msa") |
|
|
| if not os.path.exists(ner_path): |
| print("Downloading CAMEL tools NER data...") |
| os.system("camel_data -i ner-arabert") |
|
|
| if not os.path.exists(disambig_path): |
| print("Downloading CAMEL tools BERT Disambiguation data...") |
| os.system("camel_data -i disambig-bert-unfactored-msa") |
|
|
| |
| VERB_POS = {"verb", "verb_pseudo"} |
| DANGLING_PREFIXES = ('وأضافت', 'وأضاف', 'وتابع', 'وتابعت', 'وأكدت', 'وأشار', 'وأشارت', 'وبين', 'وأوضح', 'وأوضحت') |
|
|
| |
| SENTENCE_SPLIT_REGEX = re.compile(r'[.!?؟。]+') |
|
|
| |
| CONNECTORS = sorted([ |
| 'بينما', 'في حين', 'حيث إن', 'حيث أن', |
| 'كما أعلنت', 'كما أعلن', 'كما أكدت', 'كما أكد', |
| 'كما أشارت', 'كما أشار', 'كما', |
| 'وأضافت', 'وأضاف', 'وأكدت', 'وأكد', |
| 'وأشارت', 'وأشار', 'وأعلنت', 'وأعلن', |
| 'وأوضح', 'وأوضحت', 'وتابع', 'وتابعت', |
| 'وصرح', 'وصرحت', 'وأفاد', 'وأفادت', |
| 'وقال', 'وقالت', 'وأضاف أن', 'وأكد أن', |
| ], key=len, reverse=True) |
|
|
| |
| SPLIT_PATTERN = re.compile(f"( {' | '.join(CONNECTORS)} )") |
|
|
|
|
| class ClaimExtractorLogic: |
| def __init__(self): |
| print("Initializing models (Looking for GPU)...") |
| |
| self.ner = NERecognizer.pretrained() |
| self.disambig = BERTUnfactoredDisambiguator.pretrained(model_name="msa") |
| print("Models loaded.") |
|
|
| @lru_cache(maxsize=128) |
| def run(self, text: str) -> list[str]: |
| if not text or not text.strip(): |
| return [] |
|
|
| text = normalize_unicode(text) |
| text = ' '.join(text.split()) |
| base_sentences = SENTENCE_SPLIT_REGEX.split(text) |
| base_sentences = [s.strip() for s in base_sentences if s.strip()] |
|
|
| |
| flat_tokens_batch = [] |
| chunk_meta = [] |
|
|
| for sent_idx, base_sentence in enumerate(base_sentences): |
| chunks = self._layer2_split(base_sentence) |
| for chunk in chunks: |
| tokens = simple_word_tokenize(chunk) |
| if tokens: |
| flat_tokens_batch.append(tokens) |
| chunk_meta.append({ |
| "sent_idx": sent_idx, |
| "chunk": chunk, |
| "tokens": tokens |
| }) |
|
|
| if not flat_tokens_batch: |
| return [] |
|
|
| |
| try: |
| all_ner_labels = self.ner.predict_sentences(flat_tokens_batch) |
| all_analyses = self.disambig.disambiguate_sentences(flat_tokens_batch) |
| except AttributeError: |
| |
| |
| all_ner_labels = [self.ner.predict_sentence(t) for t in flat_tokens_batch] |
| all_analyses = [self.disambig.disambiguate(t) for t in flat_tokens_batch] |
|
|
| |
| claims = [] |
| seen = set() |
| current_sent_idx = -1 |
| running_context = set() |
|
|
| for idx in range(len(flat_tokens_batch)): |
| meta = chunk_meta[idx] |
| |
| |
| if meta["sent_idx"] != current_sent_idx: |
| running_context.clear() |
| current_sent_idx = meta["sent_idx"] |
|
|
| tokens = meta["tokens"] |
| ner_labels = all_ner_labels[idx] |
| analyses = all_analyses[idx] |
| chunk = meta["chunk"] |
|
|
| |
| chunk_claims, entities = self._parse_predictions(chunk, tokens, ner_labels, analyses) |
| |
| if entities: |
| running_context.update([e["text"] for e in entities]) |
|
|
| for claim in chunk_claims: |
| if claim.startswith(DANGLING_PREFIXES) and running_context: |
| context_str = "، ".join(list(running_context)[:3]) |
| claim = f"[سياق: {context_str}] {claim}" |
| |
| |
| if claim not in seen: |
| seen.add(claim) |
| claims.append(claim) |
|
|
| return claims |
|
|
| def _layer2_split(self, sentence: str) -> list[str]: |
| if len(sentence.split()) <= 20: |
| return [sentence] |
|
|
| MARKER = '<<<SPLIT>>>' |
| |
| |
| marked = SPLIT_PATTERN.sub(lambda m: f" {MARKER}{m.group(1).strip()} ", sentence) |
|
|
| chunks = [] |
| for part in marked.split(MARKER): |
| part = part.strip().rstrip('،,') |
| if part and len(part.split()) >= 4: |
| chunks.append(part) |
|
|
| return chunks if chunks else [sentence] |
|
|
| def _parse_predictions(self, chunk, tokens, ner_labels, analyses): |
| entities = self._merge_bio_entities(tokens, ner_labels) |
| |
| verbs_found = False |
| nouns_found = False |
|
|
| for i, analysis in enumerate(analyses): |
| if analysis.analyses: |
| best = analysis.analyses[0].analysis |
| pos = best.get("pos", "") |
| if pos in VERB_POS: |
| verbs_found = True |
| break |
| elif pos in {"noun", "noun_prop"}: |
| nouns_found = True |
| |
| if not verbs_found or (not entities and not nouns_found): |
| return [], entities |
|
|
| claim = dediac_ar(chunk.strip()) |
| return [claim], entities |
|
|
| def _merge_bio_entities(self, tokens: list[str], labels: list[str]) -> list[dict]: |
| entities = [] |
| curr_tokens, curr_type, curr_start = [], None, None |
|
|
| for i, (token, label) in enumerate(zip(tokens, labels)): |
| if label.startswith("B-"): |
| if curr_tokens: |
| entities.append({"text": " ".join(curr_tokens), "type": curr_type, "start": curr_start}) |
| curr_tokens, curr_type, curr_start = [token], label[2:], i |
| elif label.startswith("I-") and curr_tokens: |
| curr_tokens.append(token) |
| else: |
| if curr_tokens: |
| entities.append({"text": " ".join(curr_tokens), "type": curr_type, "start": curr_start}) |
| curr_tokens, curr_type, curr_start = [], None, None |
|
|
| if curr_tokens: |
| entities.append({"text": " ".join(curr_tokens), "type": curr_type, "start": curr_start}) |
| return entities |
|
|
|
|
| |
| extractor = ClaimExtractorLogic() |
|
|
| def predict(text): |
| try: |
| return extractor.run(text) |
| except Exception as e: |
| import traceback |
| return [f"Error: {str(e)}", traceback.format_exc()] |
|
|
| |
| demo = gr.Interface( |
| fn=predict, |
| inputs=gr.Textbox(label="Arabic News Text", lines=10), |
| outputs=gr.JSON(label="Extracted Claims"), |
| title="Arabic Claim Extractor", |
| description="Extracts atomic verifiable claims from Arabic news articles using Camel-Tools NER and POS tagging.", |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |