First Initialization

This commit is contained in:
aliakbar
2026-06-29 15:32:24 +03:30
commit 29c2add74e
105 changed files with 13177 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# modules/__init__.py
from .segmenter_old import Segmenter
from .api_handler import APIHandler
from .data_manager import DataManager
from .visualizer import Visualizer
# تعریف صریح کلاس‌هایی که با ایمپورت پکیج در دسترس قرار می‌گیرند
__all__ = [
'Segmenter',
'APIHandler',
'DataManager',
'Visualizer'
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+125
View File
@@ -0,0 +1,125 @@
import json
import re
from openai import OpenAI
from config import API_KEY, BASE_URL, MODEL_NAME
class APIHandler:
def __init__(self):
# ایجاد کلاینت با استفاده از کتابخانه رسمی OpenAI
self.client = OpenAI(
base_url=BASE_URL,
api_key=API_KEY
)
self.model_name = MODEL_NAME
def _call_api(self, prompt, system_prompt="شما یک دستیار هوش مصنوعی دقیق در تحلیل متن هستید.", temperature=0.3):
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
temperature=temperature,
timeout=60 # جلوگیری از گیر کردن برنامه در صورت کندی سرور
)
# استخراج محتوای متنی از ریسپانس استاندارد openai
return response.choices[0].message.content.strip()
except Exception as e:
print(f"API failed {e}")
return None
def get_semantic_unit(self, segment_sentences):
"""تابع ۱: استخراج واحد معنایی (برای جایگزینی در نودهای درخت)"""
text_to_summarize = " ".join(segment_sentences)
prompt = f"""تو یک دستیار هوش مصنوعی متخصص در تحلیل متن و زبان‌شناسی پردازشی هستی.
وظیفه تو این است که متن ورودی را بخوانی و تنها «هسته اصلی واحد معنایی» آن را سنتز و تولید کنی.
تعریف واحد معنایی مورد نظر: یک تک‌جملهٔ خلاصه، دقیق و جامع که یک رویداد کامل را توصیف کند. این جمله باید حتماً دارای ساختار منطقی (شامل عامل/فاعل، مفعول و فعلِ نشان‌دهنده نتیجه) باشد.
قوانین اکید خروجی (Strict Rules):
1. به هیچ‌وجه از کلمات ربط اضافی، پیش‌درآمد (مثل "جمله استخراج شده:"، "خلاصه متن:" یا "بله، حتما") و توضیحات استفاده نکن.
مثال‌ها (Few-Shot Examples):
متن ورودی: "دیروز در پی بارش شدید باران، رودخانه طغیان کرد. این اتفاق باعث شد تا تمام خانه‌های روستای مجاور زیر آب بروند و مردم مجبور به تخلیه سریع منطقه شوند."
خروجی: طغیان رودخانه بر اثر بارش باران منجر به آب‌گرفتگی خانه‌ها و تخلیه روستا شد.
متن ورودی: "تیم توسعه‌دهنده ماه‌ها روی بهینه‌سازی الگوریتم کار کرد. در نهایت موفق شدند زمان پردازش داده‌ها را به نصف کاهش دهند که این موضوع رضایت مدیران را در پی داشت."
خروجی: تلاش تیم توسعه‌دهنده برای بهینه‌سازی الگوریتم باعث کاهش نصف زمان پردازش و جلب رضایت مدیران شد.
متن ورودی اصلی:
{text_to_summarize}
"""
response = self._call_api(prompt)
return response if response else "خطا در استخراج واحد معنایی"
def extract_propositions(self, sentences):
"""تابع ۲: استخراج تک‌تک گزاره‌ها از جملات یک سگمنت (برای شروع ساخت گراف)"""
text = " ".join(sentences)
prompt = f"""
متن زیر را بخوان و تمام گزاره‌های معنایی (اقدامات، واقعیت‌ها یا مفاهیم مستقل) موجود در آن را استخراج کن.
قانون اکید: خروجی را فقط به صورت یک آرایه JSON معتبر از رشته‌ها برگردان و هیچ متن اضافه‌ای ننویس.
مثال خروجی: ["گزاره اول", "گزاره دوم"]
متن:
{text}
"""
response = self._call_api(prompt, temperature=0.2)
if not response:
return []
try:
clean_json = re.sub(r'```json|```', '', response).strip()
propositions = json.loads(clean_json)
if isinstance(propositions, list):
return propositions
return []
except Exception as e:
print(f"خطا در پارس کردن گزاره‌ها (JSON Error): {e}")
print(f"خروجی خام مدل این بود: {response}")
return []
def check_relation(self, prop1, prop2):
"""تابع ۳: بررسی رابطه غایی (هدف-وسیله) یا علت و معلولی بین دو گزاره"""
prompt = f"""
دو گزاره زیر را بررسی کن و ببین آیا رابطه "علت و معلولی" یا "غایی (هدف و وسیله)" بین آن‌ها برقرار است یا خیر.
اگر هیچ ارتباطی از این نوع وجود ندارد، دقیقاً و فقط بنویس: "ارتباط وجود ندارد"
اگر ارتباط وجود دارد، در یک گزاره کوتاه و دقیق این رابطه را توصیف کن (مثلاً: گزاره اول وسیله‌ای برای رسیدن به هدف گزاره دوم است).
گزاره ۱: {prop1}
گزاره ۲: {prop2}
"""
response = self._call_api(prompt, temperature=0.3)
return response if response else "ارتباط وجود ندارد"
def score_relation(self, relation_desc):
"""تابع ۴: امتیازدهی به رابطه از 1 تا 10"""
if not relation_desc or relation_desc.strip() == "ارتباط وجود ندارد" or relation_desc.strip() == "":
return 0
prompt = f"""
با توجه به توصیف رابطه زیر، یک نمره از 1 تا 10 به میزان قوت و منطقی بودن این رابطه علت و معلولی/غایی بده.
قانون اکید: فـــقـــط یک عدد صحیح برگردان. هیچ کاراکتر یا کلمه‌ای غیر از عدد ننویس.
توصیف رابطه:
{relation_desc}
"""
response = self._call_api(prompt, temperature=0.1)
if not response:
return 0
try:
numbers = re.findall(r'\d+', response)
if numbers:
score = int(numbers[0])
return max(1, min(10, score))
return 0
except Exception as e:
print(f"failed to extract scores {e}")
return 0
+59
View File
@@ -0,0 +1,59 @@
import os
import csv
import json
class DataManager:
def __init__(self, base_projects_dir="projects"):
self.base_projects_dir = base_projects_dir
if not os.path.exists(self.base_projects_dir):
os.makedirs(self.base_projects_dir)
def create_project_folder(self, project_name):
"""ایجاد پوشه اصلی پروژه"""
project_path = os.path.join(self.base_projects_dir, project_name)
if not os.path.exists(project_path):
os.makedirs(project_path)
return project_path
def save_tree_data(self, project_path, tree_data_list):
"""ذخیره اطلاعات کل درخت در tree_data.csv"""
file_path = os.path.join(project_path, "tree_data.csv")
headers = ["level", "segment_id", "parent_segment_id", "original_sentences", "semantic_unit"]
with open(file_path, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
for row in tree_data_list:
# تبدیل لیست جملات به رشته JSON برای ذخیره امن در CSV
row_copy = row.copy()
if isinstance(row_copy.get("original_sentences"), list):
row_copy["original_sentences"] = json.dumps(row_copy["original_sentences"], ensure_ascii=False)
writer.writerow(row_copy)
def save_graph_data(self, project_path, node_id, propositions, relations, scores):
"""ذخیره ۳ فایل CSV مربوط به گراف یک نود خاص"""
graph_dir = os.path.join(project_path, f"{node_id}_graph")
if not os.path.exists(graph_dir):
os.makedirs(graph_dir)
# 1. save propositions.csv
with open(os.path.join(graph_dir, "propositions.csv"), mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["proposition_id", "sentence", "proposition_text"])
writer.writeheader()
writer.writerows(propositions)
# 2. save relations.csv
with open(os.path.join(graph_dir, "relations.csv"), mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["relation_id", "source_proposition_id", "target_proposition_id",
"relation_type"])
writer.writeheader()
writer.writerows(relations)
# 3. save scores.csv
with open(os.path.join(graph_dir, "scores.csv"), mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["relation_id", "score"])
writer.writeheader()
writer.writerows(scores)
return graph_dir
+116
View File
@@ -0,0 +1,116 @@
import re
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
class Segmenter:
def __init__(self):
print("Loading models :")
self.model_bge = SentenceTransformer("C:/Models/bge-m3", model_kwargs={"use_safetensors": False})
self.model_e5 = SentenceTransformer("C:/Models/multilingual-e5-large")
self.model_labse = SentenceTransformer("C:/Models/LaBSE_Local")
self.model_parsroberta = SentenceTransformer("C:/Models/ParsRoberta")
print("4 models loaded successfully")
def get_similarities(self, text1, text2):
"""محاسبه میانگین شباهت کسینوسی از ۴ مدل برای دو متن داده شده"""
emb_bge = self.model_bge.encode([text1, text2])
emb_e5 = self.model_e5.encode([text1, text2])
emb_labse = self.model_labse.encode([text1, text2])
emb_parsroberta = self.model_parsroberta.encode([text1, text2])
sim_bge = cosine_similarity([emb_bge[0]], [emb_bge[1]])[0][0]
sim_e5 = cosine_similarity([emb_e5[0]], [emb_e5[1]])[0][0]
sim_labse = cosine_similarity([emb_labse[0]], [emb_labse[1]])[0][0]
sim_parsroberta = cosine_similarity([emb_parsroberta[0]], [emb_parsroberta[1]])[0][0]
# میانگین‌گیری از هر ۴ مدل
return np.mean([sim_bge, sim_e5, sim_labse, sim_parsroberta])
def calculate_depth_scores(self, similarities):
"""محاسبه عمق دره‌ها (Gap Scores) با استفاده از قله‌های چپ و راست"""
depth_scores = []
n = len(similarities)
for i in range(n):
left_peak = similarities[i]
for j in range(i, -1, -1):
if similarities[j] >= left_peak:
left_peak = similarities[j]
else:
break
right_peak = similarities[i]
for j in range(i, n):
if similarities[j] >= right_peak:
right_peak = similarities[j]
else:
break
depth = (left_peak - similarities[i]) + (right_peak - similarities[i])
depth_scores.append(depth)
return depth_scores
def extract_sentences(self, text):
"""جداسازی متن اولیه به جملات"""
text = text.replace('\n', ' ')
sentences = [s.strip() for s in re.split(r'(?<=[.!?؟]) +', text) if s.strip()]
return sentences
def segment_items(self, items, force_count=None, std_multiplier=0.5, min_depth=0.02):
"""
دریافت جملات یا واحدهای معنایی و کلاسترینگ آن‌ها.
اگر پارامتر force_count ارسال شود، الگوریتم موظف است دقیقاً همان تعداد سگمنت تولید کند.
"""
n_items = len(items)
if n_items < 2:
return [items]
similarities = []
for i in range(n_items - 1):
sim = self.get_similarities(items[i], items[i + 1])
similarities.append(sim)
depth_scores = self.calculate_depth_scores(similarities)
# -------------------------------------------------------------
# حالت اجباری: رساندن تعداد سگمنت‌ها به عدد دقیق (مثلا 7)
# -------------------------------------------------------------
if force_count and 1 < force_count < n_items:
# پیدا کردن عمیق‌ترین دره‌ها برای ایجاد دقیقا force_count - 1 برش
top_cut_indices = np.argsort(depth_scores)[-(force_count - 1):]
top_cut_indices = sorted(top_cut_indices)
grouped_segments = []
start = 0
for idx in top_cut_indices:
grouped_segments.append(items[start:idx + 1])
start = idx + 1
grouped_segments.append(items[start:])
return grouped_segments
# -------------------------------------------------------------
# حالت عادی داینامیک: در سطوح پایین‌تر که هنوز به تعداد هدف نرسیده‌ایم
# -------------------------------------------------------------
mean_depth = np.mean(depth_scores)
std_depth = np.std(depth_scores)
cutoff_threshold = mean_depth + (std_multiplier * std_depth)
grouped_segments = []
current_segment = [items[0]]
for i in range(len(similarities)):
if depth_scores[i] >= cutoff_threshold and depth_scores[i] > min_depth:
grouped_segments.append(current_segment)
current_segment = [items[i + 1]]
else:
current_segment.append(items[i + 1])
if current_segment:
grouped_segments.append(current_segment)
return grouped_segments
+216
View File
@@ -0,0 +1,216 @@
import networkx as nx
from pyvis.network import Network
import textwrap
import ast
import re
class Visualizer:
def __init__(self):
pass
def build_graph(self, propositions, relations, scores, output_path):
"""
دریافت ۳ ساختار مجزا (گزاره‌ها، روابط، امتیازها) و رسم گراف
"""
net = Network(height="100vh", width="100%", bgcolor="#1a1a1a", font_color="white", directed=True,
cdn_resources="in_line")
# ساخت دیکشنری‌های کمکی برای جستجوی سریع
prop_dict = {p['proposition_id']: p['proposition_text'] for p in propositions}
score_dict = {s['relation_id']: s['score'] for s in scores}
# 1. اضافه کردن نودها (گزاره‌ها)
for p_id, text in prop_dict.items():
wrapped_text = "\n".join(textwrap.wrap(text, width=40))
net.add_node(
p_id,
label=f"گزاره {p_id}",
title=wrapped_text, # نمایش متن کامل هنگام هاور پیش‌فرض
shape="box",
color={"background": "#264653", "border": "#1b3039", "highlight": {"background": "#2a9d8f"}},
font={"face": "B Nazanin, Tahoma, Arial", "size": 16, "color": "white"}
)
# 2. اضافه کردن یال‌ها (روابط)
for rel in relations:
source = rel['source_proposition_id']
target = rel['target_proposition_id']
rel_id = rel['relation_id']
rel_type = rel['relation_type']
score = score_dict.get(rel_id, 0)
wrapped_rel = "\n".join(textwrap.wrap(rel_type, width=40))
edge_title = f"امتیاز: {score}\n\nنوع رابطه:\n{wrapped_rel}"
if source in prop_dict and target in prop_dict:
net.add_edge(
source,
target,
label=str(score),
title=edge_title, # نمایش متن در سایدبار از طریق رویداد
width=max(2, int(score) / 2), # ضخامت خط بر اساس امتیاز
color={"color": "#e9c46a", "highlight": "#f4a261"},
font={"align": "middle", "color": "#ffea00", "background": "#1a1a1a"},
arrows={"to": {"enabled": True, "scaleFactor": 1.2}}
)
# تنظیمات گراف و فعال‌سازی هاور
net.set_options("""
{
"physics": {
"forceAtlas2Based": {
"gravitationalConstant": -50,
"centralGravity": 0.01,
"springLength": 150,
"springConstant": 0.08
},
"minVelocity": 0.75,
"solver": "forceAtlas2Based"
},
"interaction": { "hover": true, "navigationButtons": true }
}
""")
html_content = net.generate_html(output_path)
# تزریق اسکریپت برای ارسال اطلاعات هاور به سایدبار صفحه وب
hover_js = """
<script type="text/javascript">
network.on("hoverEdge", function (params) {
var edgeData = edges.get(params.edge);
window.parent.postMessage({ type: 'hoverEdge', info: edgeData.title }, '*');
});
network.on("hoverNode", function (params) {
var nodeData = nodes.get(params.node);
window.parent.postMessage({ type: 'hoverNode', info: nodeData.title }, '*');
});
network.on("blurEdge", function (params) {
window.parent.postMessage({ type: 'blur', info: '' }, '*');
});
network.on("blurNode", function (params) {
window.parent.postMessage({ type: 'blur', info: '' }, '*');
});
</script>
</body>
"""
html_content = html_content.replace('</body>', hover_js)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_content)
return output_path
def build_tree(self, tree_data_list, output_path):
net = Network(height="100vh", width="100%", bgcolor="#1a1a1a", font_color="white", directed=True,
cdn_resources="in_line")
valid_node_ids = set()
for node_data in tree_data_list:
node_id = str(node_data.get('segment_id', '')).strip()
valid_node_ids.add(node_id)
level = int(node_data.get('level', 0))
full_text = node_data.get('semantic_unit', 'بدون متن')
wrapped_text = "\n".join(textwrap.wrap(full_text, width=40))
raw_sentences = node_data.get('original_sentences', [])
if isinstance(raw_sentences, str):
try:
raw_sentences = ast.literal_eval(raw_sentences)
except (ValueError, SyntaxError):
pass
if isinstance(raw_sentences, list):
sentences_str = "<br><br>".join(raw_sentences)
num_sentences = len(raw_sentences)
else:
sentences_str = str(raw_sentences)
num_sentences = len([s for s in sentences_str.split('.') if s.strip()]) if sentences_str else 0
# متمایز کردن رنگ برگ‌ها (سطح 0) از نودهای میانی
bg_color = "#e76f51" if level == 0 else "#2a9d8f"
net.add_node(
node_id,
label=f"سطح {level}\n{node_id}",
title=wrapped_text,
level=level,
shape="box",
sentences=sentences_str,
num_sentences=num_sentences,
color={"background": bg_color, "border": "#1f6e64", "highlight": {"background": "#3bc2b4"}},
font={"face": "B Nazanin, Tahoma, Arial", "size": 16}
)
for node_data in tree_data_list:
node_id = str(node_data.get('segment_id', '')).strip()
parent_val = node_data.get('parent_segment_id')
if parent_val and str(parent_val).strip() != "":
clean_parents_str = re.sub(r"[\[\]\'\"\s]", "", str(parent_val))
parents_list = clean_parents_str.split(',')
for p_id in parents_list:
if p_id in valid_node_ids:
net.add_edge(
p_id,
node_id,
width=3,
color={"color": "#e9c46a", "highlight": "#f4a261"},
arrows={"to": {"enabled": True, "scaleFactor": 1.2}},
smooth={'type': 'cubicBezier', 'forceDirection': 'vertical', 'roundness': 0.4}
)
net.set_options("""
{
"layout": {
"hierarchical": {
"enabled": true,
"direction": "DU",
"sortMethod": "directed",
"nodeSpacing": 250,
"levelSeparation": 150
}
},
"physics": {
"hierarchicalRepulsion": {
"centralGravity": 0.0,
"springLength": 100,
"springConstant": 0.01,
"nodeDistance": 250,
"damping": 0.09
},
"solver": "hierarchicalRepulsion"
},
"interaction": { "hover": true, "navigationButtons": true }
}
""")
html_content = net.generate_html(output_path)
# تزریق اسکریپت کلیک درخت
click_js = """
<script type="text/javascript">
network.on("click", function (params) {
if (params.nodes.length > 0) {
var nodeId = params.nodes[0];
var clickedNode = nodes.get(nodeId);
window.parent.postMessage({
type: 'node_click',
id: nodeId,
label: clickedNode.label,
summary: clickedNode.title,
sentences: clickedNode.sentences || 'اطلاعات جملات موجود نیست.',
num_sentences: clickedNode.num_sentences || 0
}, '*');
}
});
</script>
</body>
"""
html_content = html_content.replace('</body>', click_js)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_content)
return output_path