diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..038af1a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,313 @@
+# local
+files/*
+logs/*
+models/mnli_model
+models/saved_models
+notebooks/files
+notebooks/results
+notebooks/word_files
+notebooks/document_topics.csv
+notebooks/g.ipynb
+notebooks/ocr_processing.ipynb
+notebooks/topic_modeling_analysis.ipynb
+results/*
+tests/*
+notebooks/logs
+notebooks/output_files
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+
+# AWS User-specific
+
+# Generated files
+
+# Sensitive or high-churn files
+
+# Gradle
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+
+# Mongo Explorer plugin
+
+# File-based project format
+
+# IntelliJ
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Cursive Clojure plugin
+
+# SonarLint plugin
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+# Editor-based Rest Client
+
+# Android studio 3.1+ serialized cache file
+
+### JupyterNotebooks template
+# gitignore template for Jupyter Notebooks
+# website: http://jupyter.org/
+
+.ipynb_checkpoints
+*/.ipynb_checkpoints/*
+
+# IPython
+profile_default/
+ipython_config.py
+
+# Remove previous ipynb_checkpoints
+# git rm -r .ipynb_checkpoints/
+
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+
+# IPython
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/AI-Data-Security.iml b/.idea/AI-Data-Security.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/.idea/AI-Data-Security.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/Topic-Modeling.iml b/.idea/Topic-Modeling.iml
new file mode 100644
index 0000000..4687102
--- /dev/null
+++ b/.idea/Topic-Modeling.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/csv-editor.xml b/.idea/csv-editor.xml
new file mode 100644
index 0000000..0153af6
--- /dev/null
+++ b/.idea/csv-editor.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..49f715b
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..348d480
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..ae77430
--- /dev/null
+++ b/app.py
@@ -0,0 +1,111 @@
+import datetime
+import os
+
+import pandas as pd
+import streamlit as st
+
+from config import Config
+from logger import logger
+from models.categorizer import Categorizer
+from models.topic_modeler import TopicModeler
+from processors.integrated_processor import IntegratedProcessor
+from readers.word_document_reader import WordDocumentReader
+
+
+def process_documents(folder_path, predefined_topics):
+ try:
+ logger.info("شروع اجرای برنامه مدلسازی موضوعات.")
+ document_reader = WordDocumentReader()
+
+ logger.info("اجرای با موضوعات از پیش تعریف شده...")
+ topic_modeler_manual = TopicModeler(predefined_topics=predefined_topics)
+ categorizer_manual = Categorizer(topic_modeler_manual)
+ manual_processor = IntegratedProcessor(
+ topic_modeler=topic_modeler_manual,
+ categorizer=categorizer_manual,
+ document_reader=document_reader,
+ folder_path=folder_path,
+ predefined_topics=predefined_topics
+ )
+ manual_results = manual_processor.run()
+
+ logger.info("تمامی فرآیندهای مدلسازی موضوعات با موفقیت انجام شد.")
+
+ # Combine results
+ combined_df = pd.concat([manual_results], ignore_index=True)
+
+ if 'Topic Label' in combined_df.columns and 'Assigned Label' not in combined_df.columns:
+ combined_df.rename(columns={'Topic Label': 'Assigned Label'}, inplace=True)
+
+ combined_df = combined_df.sort_values('Confidence', ascending=False).reset_index(drop=True)
+
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ os.makedirs(Config.RESULTS_DIR, exist_ok=True)
+ output_file = os.path.join(Config.RESULTS_DIR, f'topic_results_{timestamp}.csv')
+ combined_df.to_csv(output_file, index=False)
+ logger.info(f"نتایج در فایل '{output_file}' ذخیره شد.")
+
+ label_column = 'Assigned Label' if 'Assigned Label' in combined_df.columns else 'Topic Label'
+ grouped = combined_df.groupby(label_column)
+ grouped_data = {label: group['Document'].tolist() for label, group in grouped}
+
+ results = {
+
+ "manual": manual_results,
+
+ "combined_df": combined_df,
+ "output_file": output_file,
+ "grouped_data": grouped_data
+ }
+
+ return results
+
+ except Exception as e:
+ logger.error(f"خطایی در اجرای اصلی رخ داده است: {str(e)}")
+ st.error(f"خطایی رخ داده است. لطفاً فایل لاگ را بررسی کنید: {Config.LOG_DIR}")
+ return None
+
+
+def main():
+ st.set_page_config(page_title="سیستم مدلسازی موضوعات", layout="wide")
+ st.title("سیستم مدلسازی موضوعات برای فایلهای PDF و Word")
+
+ st.subheader("تعریف موضوعات از پیش تعریف شده")
+ st.write("هر موضوع را در یک خط جداگانه وارد کنید.")
+ predefined_topics_input = st.text_area(
+ "موضوعات از پیش تعریف شده (هر خط یک موضوع)",
+ value="فناوری\nسلامت و سبک زندگی\nهنر و سرگرمی\nسفر و مکانها\nآموزش"
+ )
+ predefined_topics = [{'label': topic.strip()} for topic in predefined_topics_input.split('\n') if topic.strip()]
+ folder_path = './notebooks/word_files/'
+ if st.button("پردازش فایلها"):
+ if os.path.exists(folder_path):
+ with st.spinner("در حال پردازش فایلها..."):
+ results = process_documents(folder_path, predefined_topics)
+ if results:
+ st.success("مدلسازی موضوعات با موفقیت انجام شد.")
+
+ st.subheader("نتایج مدلسازی موضوعات")
+ st.dataframe(results['combined_df'])
+
+ with open(results['output_file'], "rb") as file:
+ btn = st.download_button(
+ label="دانلود نتایج به صورت CSV",
+ data=file,
+ file_name=os.path.basename(results['output_file']),
+ mime="text/csv"
+ )
+
+ st.subheader("نتایج دستهبندی اسناد بر اساس دستهها")
+ for label, documents in results['grouped_data'].items():
+ with st.expander(f"دسته: {label}"):
+ for doc in documents:
+ st.write(f"- {doc}")
+ else:
+ st.error("خطایی در پردازش فایلها رخ داده است.")
+ else:
+ st.error("مسیر پوشه مشخص شده وجود ندارد. لطفاً مسیر معتبر وارد کنید.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..acfcfc8
--- /dev/null
+++ b/config.py
@@ -0,0 +1,14 @@
+# config.py
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+class Config:
+ MODEL_NAME: str = os.getenv('MODEL_NAME', './models/saved_models')
+ LOG_DIR: str = os.getenv('LOG_DIR', 'logs')
+ RESULTS_DIR: str = os.getenv('RESULTS_DIR', 'results')
+ ZERO_SHOT_MODEL: str = os.getenv('ZERO_SHOT_MODEL', './models/mnli_model')
+ SUMMARIZER_MODEL:str=os.getenv('SUMMARIZER_MODEL','')
diff --git a/interfaces/categorizer_interface.py b/interfaces/categorizer_interface.py
new file mode 100644
index 0000000..a222d5c
--- /dev/null
+++ b/interfaces/categorizer_interface.py
@@ -0,0 +1,15 @@
+from abc import ABC, abstractmethod
+from typing import List, Dict, Tuple
+
+import numpy as np
+
+
+class ICategorizer(ABC):
+ @abstractmethod
+ def assign_automatic_labels(self):
+ pass
+
+ @abstractmethod
+ def assign_predefined_labels(self, embeddings: np.ndarray, predefined_topics: List[Dict]) -> Tuple[
+ List[str], List[float]]:
+ pass
diff --git a/interfaces/document_reader_interface.py b/interfaces/document_reader_interface.py
new file mode 100644
index 0000000..4358583
--- /dev/null
+++ b/interfaces/document_reader_interface.py
@@ -0,0 +1,9 @@
+from abc import ABC, abstractmethod
+from typing import List
+
+
+class IDocumentReader(ABC):
+
+ @abstractmethod
+ def read_documents(self, folder_path: str) -> List[str]:
+ pass
diff --git a/interfaces/summarizer_interface.py b/interfaces/summarizer_interface.py
new file mode 100644
index 0000000..932cfac
--- /dev/null
+++ b/interfaces/summarizer_interface.py
@@ -0,0 +1,9 @@
+# interfaces/summarizer_interface.py
+from abc import ABC, abstractmethod
+from typing import List
+
+
+class ISummarizer(ABC):
+ @abstractmethod
+ def summarize(self, texts: List[str]) -> List[str]:
+ pass
diff --git a/interfaces/topic_modeler_interface.py b/interfaces/topic_modeler_interface.py
new file mode 100644
index 0000000..b11f1eb
--- /dev/null
+++ b/interfaces/topic_modeler_interface.py
@@ -0,0 +1,19 @@
+from abc import ABC, abstractmethod
+from typing import List, Dict, Optional, Tuple
+
+import numpy as np
+import pandas as pd
+
+
+class ITopicModeler(ABC):
+ @abstractmethod
+ def fit_transform(self, documents: List[str]) -> Tuple[List[int], np.ndarray, np.ndarray]:
+ pass
+
+ @abstractmethod
+ def get_topic_info(self) -> Optional[pd.DataFrame]:
+ pass
+
+ @abstractmethod
+ def get_topics(self) -> Optional[Dict[int, List[tuple]]]:
+ pass
diff --git a/logger.py b/logger.py
new file mode 100644
index 0000000..5f9d806
--- /dev/null
+++ b/logger.py
@@ -0,0 +1,28 @@
+import datetime
+import logging
+import os
+
+from config import Config
+
+
+def setup_logging():
+
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ log_filename = f'topic_modeling_{timestamp}.log'
+ os.makedirs(Config.LOG_DIR, exist_ok=True)
+ log_path = os.path.join(Config.LOG_DIR, log_filename)
+
+ logging.basicConfig(
+ filename=log_path,
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+ console = logging.StreamHandler()
+ console.setLevel(logging.INFO)
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+ return log_path
+log_path = setup_logging()
+logger = logging.getLogger(__name__)
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..378b6ea
--- /dev/null
+++ b/main.py
@@ -0,0 +1,66 @@
+import logging
+from models.topic_modeler import TopicModeler
+from models.categorizer import Categorizer
+from readers.pdf_document_reader import WordDocumentReader
+from summarizers.transformers_summarizer import TransformersSummarizer
+from processors.integrated_processor import IntegratedProcessor
+from logger import logger
+from config import Config
+
+def main():
+ pdf_folder_path = './notebooks/word_files/'
+ predefined_topics = [
+ {'label': 'فناوری'},
+ {'label': 'سلامت و سبک زندگی'},
+ {'label': 'هنر و سرگرمی'},
+ {'label': 'سفر و مکانها'},
+ {'label': 'آموزش'}
+ ]
+
+ try:
+ logger.info("شروع اجرای برنامه مدلسازی موضوعات.")
+ document_reader = WordDocumentReader()
+ logger.info("اجرای تشخیص خودکار موضوعات...")
+ topic_modeler_auto = TopicModeler()
+ categorizer_auto = Categorizer(topic_modeler_auto)
+ auto_processor = IntegratedProcessor(
+ topic_modeler=topic_modeler_auto,
+ categorizer=categorizer_auto,
+ document_reader=document_reader,
+ #summarizer=summarizer,
+ folder_path=pdf_folder_path
+ )
+ auto_results = auto_processor.run()
+ print(auto_results)
+ logger.info("اجرای با موضوعات از پیش تعریف شده...")
+ topic_modeler_manual = TopicModeler(predefined_topics=predefined_topics)
+ categorizer_manual = Categorizer(topic_modeler_manual)
+ manual_processor = IntegratedProcessor(
+ topic_modeler=topic_modeler_manual,
+ categorizer=categorizer_manual,
+ document_reader=document_reader,
+ #summarizer=summarizer,
+ folder_path=pdf_folder_path,
+ predefined_topics=predefined_topics
+ )
+ manual_results = manual_processor.run()
+ logger.info("اجرای با طبقهبندی صفر-شات برای برچسبگذاری...")
+ topic_modeler_zero_shot = TopicModeler()
+ categorizer_zero_shot = Categorizer(topic_modeler_zero_shot)
+ zero_shot_processor = IntegratedProcessor(
+ topic_modeler=topic_modeler_zero_shot,
+ categorizer=categorizer_zero_shot,
+ document_reader=document_reader,
+ #summarizer=summarizer,
+ folder_path=pdf_folder_path
+ )
+ zero_shot_results = zero_shot_processor.run(use_zero_shot=True)
+
+ logger.info("تمامی فرآیندهای مدلسازی موضوعات با موفقیت انجام شد.")
+
+ except Exception as e:
+ logger.error(f"خطایی در اجرای اصلی رخ داده است: {str(e)}")
+ print(f"خطایی رخ داده است. لطفاً فایل لاگ را بررسی کنید: {Config.LOG_DIR}")
+
+if __name__ == "__main__":
+ main()
diff --git a/models/categorizer.py b/models/categorizer.py
new file mode 100644
index 0000000..86987cf
--- /dev/null
+++ b/models/categorizer.py
@@ -0,0 +1,106 @@
+# models/categorizer.py
+import logging
+from typing import List, Dict, Tuple
+
+import numpy as np
+from transformers import pipeline
+
+from config import Config
+from interfaces.categorizer_interface import ICategorizer
+
+logger = logging.getLogger(__name__)
+
+
+class Categorizer(ICategorizer):
+
+ def __init__(self, topic_modeler: 'ITopicModeler'):
+
+ self.topic_modeler = topic_modeler
+ self.topic_labels = {}
+ self.zero_shot_classifier = None
+ self.load_zero_shot_classifier()
+
+ def load_zero_shot_classifier(self):
+ try:
+ logger.info("بارگذاری مدل طبقهبندی صفر-شات.")
+ self.zero_shot_classifier = pipeline(
+ "zero-shot-classification",
+ model=Config.ZERO_SHOT_MODEL,
+ )
+ logger.info("مدل طبقهبندی صفر-شات با موفقیت بارگذاری شد.")
+ except Exception as e:
+ logger.error(f"خطا در بارگذاری مدل طبقهبندی صفر-شات: {str(e)}")
+ raise
+
+ def assign_automatic_labels(self):
+ try:
+ logger.info("شروع تخصیص خودکار برچسبها.")
+ topic_info = self.topic_modeler.get_topic_info()
+ if topic_info is None:
+ raise ValueError("عدم توانایی در دریافت اطلاعات موضوعات.")
+
+ for _, row in topic_info.iterrows():
+ topic_id = row['Topic']
+
+ if topic_id == -1:
+ self.topic_labels[topic_id] = 'خارج از دستهبندی'
+ continue
+
+ topic_words = [word for word, _ in self.topic_modeler.get_topics()[topic_id]][:10]
+
+ labels = topic_words
+
+ topic_desc = " ".join(topic_words)
+
+ classification = self.zero_shot_classifier(
+ sequences=topic_desc,
+ candidate_labels=labels,
+ multi_label=True
+ )
+ if classification['labels']:
+ best_label = classification['labels'][0]
+ else:
+ best_label = f"موضوع {topic_id}"
+
+ self.topic_labels[topic_id] = best_label
+
+ logger.debug(f"موضوع {topic_id} با برچسب '{best_label}' تخصیص داده شد.")
+
+ logger.info("تخصیص خودکار برچسبها با موفقیت انجام شد.")
+ except Exception as e:
+ logger.error(f"خطا در تخصیص خودکار برچسبها: {str(e)}")
+ raise
+
+ def assign_predefined_labels(self, embeddings: np.ndarray, predefined_topics: List[Dict]) -> Tuple[
+ List[str], List[float]]:
+ try:
+ logger.info("شروع تخصیص برچسبهای از پیش تعریف شده.")
+ if not predefined_topics:
+ raise ValueError("موضوعات از پیش تعریف شدهای ارائه نشده است.")
+
+ topic_labels = [topic['label'] for topic in predefined_topics]
+ topic_label_embeddings = self.topic_modeler.embedding_model.encode(
+ topic_labels, show_progress_bar=False
+ )
+ logger.debug(f"تعبیهسازی برچسبهای موضوعات از پیش تعریف شده انجام شد.")
+
+ embeddings_norm = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
+ topic_label_embeddings_norm = topic_label_embeddings / np.linalg.norm(topic_label_embeddings, axis=1,
+ keepdims=True)
+
+ similarities = np.dot(embeddings_norm, topic_label_embeddings_norm.T)
+ logger.debug("محاسبه شباهت کسینوسی بین اسناد و برچسبها انجام شد.")
+
+ assigned_labels = []
+ confidences = []
+ for sim in similarities:
+ idx = np.argmax(sim)
+ assigned_labels.append(topic_labels[idx])
+ confidences.append(sim[idx])
+
+ logger.info("تخصیص برچسبهای از پیش تعریف شده با موفقیت انجام شد.")
+ return assigned_labels, confidences
+
+ except Exception as e:
+ logger.error(f"خطا در تخصیص برچسبهای از پیش تعریف شده: {str(e)}")
+ raise
diff --git a/models/topic_modeler.py b/models/topic_modeler.py
new file mode 100644
index 0000000..cfef77f
--- /dev/null
+++ b/models/topic_modeler.py
@@ -0,0 +1,92 @@
+# models/topic_modeler.py
+import logging
+from typing import List, Dict, Optional, Tuple
+
+import numpy as np
+import pandas as pd
+from bertopic import BERTopic
+from sentence_transformers import SentenceTransformer
+from sklearn.feature_extraction.text import CountVectorizer
+
+from config import Config
+from interfaces.topic_modeler_interface import ITopicModeler
+
+logger = logging.getLogger(__name__)
+
+
+class TopicModeler(ITopicModeler):
+
+ def __init__(self, predefined_topics: Optional[List[Dict]] = None):
+
+ try:
+ logger.info("شروع به بارگذاری مدل تعبیهسازی جملات.")
+ self.embedding_model = SentenceTransformer(Config.MODEL_NAME)
+ logger.info(f"مدل تعبیهسازی '{Config.MODEL_NAME}' با موفقیت بارگذاری شد.")
+
+ self.predefined_topics = predefined_topics
+
+ persian_stop_words = [
+ 'و', 'در', 'به', 'از', 'که', 'با', 'برای', 'این', 'را', 'آن', 'ها', 'یک', 'می', 'تا', 'بر', 'یا', 'اما',
+ 'دیگر', 'هم', 'کرده', 'کرد', 'بود', 'بودن', 'باشه', 'نیز', 'از', 'باشد'
+ ]
+
+ vectorizer = CountVectorizer(
+ stop_words=persian_stop_words,
+ ngram_range=(1, 3),
+ max_features=5000
+ )
+
+ self.topic_model = BERTopic(
+ embedding_model=self.embedding_model,
+ vectorizer_model=vectorizer,
+ calculate_probabilities=True,
+ verbose=False,
+ min_topic_size=2,
+ top_n_words=10,
+ nr_topics=10
+ )
+
+ logger.info(f"BERTopic با مدل '{Config.MODEL_NAME}' مقداردهی اولیه شد.")
+ mode = 'موضوعات از پیش تعریف شده' if predefined_topics else 'تشخیص خودکار موضوعات'
+ logger.info(f"حالت مدلسازی: {mode}")
+
+ except Exception as e:
+ logger.error(f"خطا در مقداردهی اولیه TopicModeler: {str(e)}")
+ raise
+
+ def fit_transform(self, documents: List[str]) -> Tuple[List[int], np.ndarray, np.ndarray]:
+
+ try:
+ logger.info("شروع فرآیند مدلسازی موضوعات.")
+ valid_docs = [str(doc).strip() for doc in documents if str(doc).strip()]
+ if not valid_docs:
+ raise ValueError("هیچ سند معتبر برای پردازش ارائه نشده است.")
+
+ logger.info(f"{len(valid_docs)} سند برای پردازش موجود است.")
+ embeddings = self.embedding_model.encode(valid_docs, show_progress_bar=True)
+ logger.info("تعبیهسازی اسناد تکمیل شد.")
+
+ topics, probs = self.topic_model.fit_transform(valid_docs, embeddings)
+ logger.info("مدلسازی موضوعات با BERTopic انجام شد.")
+ return topics, probs, embeddings
+ except Exception as e:
+ logger.error(f"خطا در fit_transform: {str(e)}")
+ raise
+
+ def get_topic_info(self) -> Optional[pd.DataFrame]:
+ try:
+ topic_info = self.topic_model.get_topic_info()
+ logger.info("دریافت اطلاعات موضوعات موفقیتآمیز بود.")
+ return topic_info
+ except Exception as e:
+ logger.error(f"خطا در دریافت اطلاعات موضوعات: {str(e)}")
+ return None
+
+ def get_topics(self) -> Optional[Dict[int, List[tuple]]]:
+ try:
+ topics = self.topic_model.get_topics()
+ logger.info("دریافت کلمات کلیدی موضوعات موفقیتآمیز بود.")
+ return topics
+ except Exception as e:
+ logger.error(f"خطا در دریافت موضوعات: {str(e)}")
+ return None
diff --git a/notebooks/Vazir-Light.ttf b/notebooks/Vazir-Light.ttf
new file mode 100644
index 0000000..e177452
Binary files /dev/null and b/notebooks/Vazir-Light.ttf differ
diff --git a/notebooks/data_preprocessing.ipynb b/notebooks/data_preprocessing.ipynb
new file mode 100644
index 0000000..54f657b
--- /dev/null
+++ b/notebooks/data_preprocessing.ipynb
@@ -0,0 +1,37 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "initial_id",
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/feature_experiments.ipynb b/notebooks/feature_experiments.ipynb
new file mode 100644
index 0000000..54f657b
--- /dev/null
+++ b/notebooks/feature_experiments.ipynb
@@ -0,0 +1,37 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "initial_id",
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/processors/integrated_processor.py b/processors/integrated_processor.py
new file mode 100644
index 0000000..a61ecae
--- /dev/null
+++ b/processors/integrated_processor.py
@@ -0,0 +1,114 @@
+# processors/integrated_processor.py
+import datetime
+import logging
+import os
+from typing import List, Optional, Dict
+
+import pandas as pd
+
+from config import Config
+from interfaces.categorizer_interface import ICategorizer
+from interfaces.document_reader_interface import IDocumentReader
+# from interfaces.summarizer_interface import ISummarizer
+from interfaces.topic_modeler_interface import ITopicModeler
+
+logger = logging.getLogger(__name__)
+
+
+class IntegratedProcessor:
+
+ def __init__(
+ self,
+ topic_modeler: ITopicModeler,
+ categorizer: ICategorizer,
+ document_reader: IDocumentReader,
+ # summarizer: Optional[ISummarizer],
+ folder_path: str,
+ predefined_topics: Optional[List[Dict]] = None
+ ):
+
+ self.folder_path = folder_path
+ self.predefined_topics = predefined_topics
+ self.topic_modeler = topic_modeler
+ self.categorizer = categorizer
+ self.document_reader = document_reader
+ # self.summarizer = summarizer
+ self.documents = self.document_reader.read_documents(self.folder_path)
+
+ # if self.summarizer:
+ # logger.info("شروع خلاصهسازی اسناد.")
+ # self.documents = self.summarizer.summarize(self.documents)
+
+ def run(self, use_zero_shot: bool = False) -> pd.DataFrame:
+
+ try:
+ logger.info("شروع فرآیند یکپارچه مدلسازی موضوعات و دستهبندی.")
+ if not self.documents:
+ logger.warning("هیچ سندی برای پردازش یافت نشد.")
+ return pd.DataFrame()
+
+ topics, probabilities, embeddings = self.topic_modeler.fit_transform(self.documents)
+
+ if use_zero_shot:
+ logger.info("استفاده از طبقهبندی صفر-شات برای تخصیص برچسبها.")
+ self.categorizer.assign_automatic_labels()
+ labeled_topics = [self.categorizer.topic_labels.get(topic, f"موضوع {topic}") for topic in topics]
+ assigned_topic_probs = [
+ probabilities[i][topic] if topic in probabilities[i] else 0
+ for i, topic in enumerate(topics)
+ ]
+ df = pd.DataFrame({
+ 'Document': self.documents,
+ 'Topic': topics,
+ 'Assigned Label': labeled_topics,
+ 'Confidence': assigned_topic_probs
+ })
+ elif self.predefined_topics:
+ logger.info("استفاده از برچسبهای از پیش تعریف شده برای تخصیص برچسبها.")
+ assigned_labels, confidences = self.categorizer.assign_predefined_labels(embeddings,
+ self.predefined_topics)
+ df = pd.DataFrame({
+ 'Document': self.documents,
+ 'Assigned Label': assigned_labels,
+ 'Confidence': confidences
+ })
+ else:
+ logger.info("استفاده از تخصیص برچسب خودکار بدون طبقهبندی صفر-شات.")
+ self.categorizer.assign_automatic_labels()
+ labeled_topics = [self.categorizer.topic_labels.get(topic, f"موضوع {topic}") for topic in topics]
+ assigned_topic_probs = [
+ probabilities[i][topic] if topic in probabilities[i] else 0
+ for i, topic in enumerate(topics)
+ ]
+ df = pd.DataFrame({
+ 'Document': self.documents,
+ 'Topic': topics,
+ 'Topic Label': labeled_topics,
+ 'Confidence': assigned_topic_probs
+ })
+
+ # مرتبسازی و ذخیره نتایج
+ df = df.sort_values('Confidence', ascending=False).reset_index(drop=True)
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ os.makedirs(Config.RESULTS_DIR, exist_ok=True)
+ output_file = os.path.join(Config.RESULTS_DIR, f'topic_results_{timestamp}.csv')
+ df.to_csv(output_file, index=False)
+ logger.info(f"نتایج در فایل '{output_file}' ذخیره شد.")
+
+ # گروهبندی و نمایش اسناد بر اساس دستهها
+ label_column = 'Assigned Label' if 'Assigned Label' in df.columns else 'Topic Label'
+ grouped = df.groupby(label_column)
+ print("\nنتایج دستهبندی اسناد بر اساس دستهها:")
+ print("=" * 80)
+ for label, group in grouped:
+ print(f"\nدسته: {label}")
+ print("-" * 40)
+ for doc in group['Document']:
+ print(f"- {doc}")
+
+ logger.info("فرآیند یکپارچه با موفقیت پایان یافت.")
+ return df
+
+ except Exception as e:
+ logger.error(f"خطا در پردازش اسناد: {str(e)}")
+ raise
diff --git a/readers/pdf_document_reader.py b/readers/pdf_document_reader.py
new file mode 100644
index 0000000..da191a6
--- /dev/null
+++ b/readers/pdf_document_reader.py
@@ -0,0 +1,39 @@
+ #readers/pdf_document_reader.py
+import logging
+import os
+from typing import List
+
+import pdfplumber
+
+from interfaces.document_reader_interface import IDocumentReader
+
+logger = logging.getLogger(__name__)
+
+
+class PDFDocumentReader(IDocumentReader):
+
+ def read_documents(self, folder_path: str) -> List[str]:
+
+ logger.info(f"شروع خواندن فایلهای PDF از فولدر: {folder_path}")
+ documents = []
+ try:
+ for filename in os.listdir(folder_path):
+ if filename.lower().endswith('.pdf'):
+ file_path = os.path.join(folder_path, filename)
+ logger.info(f"خواندن فایل: {file_path}")
+ with pdfplumber.open(file_path) as pdf:
+ text = ''
+ for page in pdf.pages:
+ extracted_text = page.extract_text()
+ if extracted_text:
+ text += extracted_text + ' '
+ if text.strip():
+ documents.append(text.strip())
+ logger.info(f"متن فایل '{filename}' با موفقیت استخراج شد.")
+ else:
+ logger.warning(f"هیچ متنی از فایل '{filename}' استخراج نشد.")
+ logger.info("تمامی فایلهای PDF با موفقیت خوانده شدند.")
+ except Exception as e:
+ logger.error(f"خطا در خواندن فایلهای PDF: {str(e)}")
+ raise
+ return documents
diff --git a/readers/word_document_reader.py b/readers/word_document_reader.py
new file mode 100644
index 0000000..61bcefa
--- /dev/null
+++ b/readers/word_document_reader.py
@@ -0,0 +1,42 @@
+# readers/word_document_reader.py
+import logging
+import os
+from typing import List
+
+from docx import Document
+
+from interfaces.document_reader_interface import IDocumentReader
+
+# interfaces/document_reader_interface.py
+
+logger = logging.getLogger(__name__)
+
+
+class WordDocumentReader(IDocumentReader):
+
+ def read_documents(self, folder_path: str) -> List[str]:
+
+ logger.info(f"شروع خواندن فایلهای Word از فولدر: {folder_path}")
+ documents = []
+ try:
+ for filename in os.listdir(folder_path):
+ if filename.lower().endswith('.docx'):
+ file_path = os.path.join(folder_path, filename)
+ logger.info(f"خواندن فایل: {file_path}")
+ try:
+ doc = Document(file_path)
+ text = ''
+ for paragraph in doc.paragraphs:
+ text += paragraph.text + ' '
+ if text.strip():
+ documents.append(text.strip())
+ logger.info(f"متن فایل '{filename}' با موفقیت استخراج شد.")
+ else:
+ logger.warning(f"هیچ متنی از فایل '{filename}' استخراج نشد.")
+ except Exception as e:
+ logger.error(f"خطا در خواندن فایل '{filename}': {str(e)}")
+ logger.info("تمامی فایلهای Word با موفقیت خوانده شدند.")
+ except Exception as e:
+ logger.error(f"خطا در خواندن فایلهای Word: {str(e)}")
+ raise
+ return documents
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..912a7df
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,11 @@
+bertopic==0.16.4
+fpdf==1.7.2
+numpy==1.24.3
+pandas==2.2.3
+pdfplumber==0.11.4
+python-dotenv==1.0.1
+python_docx==1.1.2
+scikit_learn==1.5.2
+sentence_transformers==3.1.0
+streamlit==1.39.0
+transformers==4.44.2
diff --git a/summarizers/test_transformers_summarizer.py b/summarizers/test_transformers_summarizer.py
new file mode 100644
index 0000000..1dd0501
--- /dev/null
+++ b/summarizers/test_transformers_summarizer.py
@@ -0,0 +1,31 @@
+# tests/test_transformers_summarizer.py
+import unittest
+
+from summarizers.transformers_summarizer import TransformersSummarizer
+
+
+class TestTransformersSummarizer(unittest.TestCase):
+
+ def setUp(self):
+ # استفاده از یک مدل خلاصهسازی کوچک برای تست
+ self.summarizer = TransformersSummarizer(model_name="facebook/bart-large-cnn")
+ self.texts = [
+ "این یک متن طولانی برای تست خلاصهسازی است. هدف ما اطمینان از صحت عملکرد خلاصهساز است.",
+ "پایتون یک زبان برنامهنویسی قدرتمند و منعطف است که در زمینههای مختلفی مورد استفاده قرار میگیرد."
+ ]
+
+ def test_summarize(self):
+ summaries = self.summarizer.summarize(self.texts)
+ self.assertEqual(len(summaries), len(self.texts))
+ for summary, original in zip(summaries, self.texts):
+ self.assertTrue(len(summary) < len(original))
+ self.assertIsInstance(summary, str)
+
+ def test_summarize_empty_text(self):
+ summaries = self.summarizer.summarize([""])
+ self.assertEqual(len(summaries), 1)
+ self.assertEqual(summaries[0], "")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/summarizers/transformers_summarizer.py b/summarizers/transformers_summarizer.py
new file mode 100644
index 0000000..dbe7834
--- /dev/null
+++ b/summarizers/transformers_summarizer.py
@@ -0,0 +1,40 @@
+# summarizers/transformers_summarizer.py
+import logging
+from typing import List
+
+from transformers import pipeline
+
+from config import Config
+from interfaces.summarizer_interface import ISummarizer
+
+logger = logging.getLogger(__name__)
+
+
+class TransformersSummarizer(ISummarizer):
+
+
+ def __init__(self, model_name: str = Config.SUMMARIZER_MODEL):
+
+ try:
+ logger.info(f"بارگذاری مدل خلاصهسازی: {model_name}")
+ self.summarizer = pipeline("summarization", model=model_name)
+ logger.info("مدل خلاصهسازی با موفقیت بارگذاری شد.")
+ except Exception as e:
+ logger.error(f"خطا در بارگذاری مدل خلاصهسازی: {str(e)}")
+ raise
+
+ def summarize(self, texts: List[str]) -> List[str]:
+
+ logger.info("شروع خلاصهسازی متون.")
+ summaries = []
+ try:
+ for text in texts:
+ # توجه: برخی مدلها محدودیت طول ورودی دارند. ممکن است نیاز به تقسیم متن به بخشهای کوچکتر باشد.
+ summary = self.summarizer(text, max_length=130, min_length=30, do_sample=False)
+ summaries.append(summary[0]['summary_text'])
+ logger.debug(f"خلاصه متن: {summary[0]['summary_text']}")
+ logger.info("خلاصهسازی متون با موفقیت انجام شد.")
+ return summaries
+ except Exception as e:
+ logger.error(f"خطا در خلاصهسازی متون: {str(e)}")
+ raise