File size: 16,725 Bytes
6ceefb7
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
bec85f3
 
6ceefb7
 
 
bec85f3
 
 
6ceefb7
 
 
 
bec85f3
 
 
 
 
 
 
6ceefb7
bec85f3
 
 
 
 
 
 
 
 
 
 
 
 
 
6ceefb7
 
bec85f3
6ceefb7
 
 
 
 
bec85f3
 
6ceefb7
bec85f3
 
6ceefb7
bec85f3
 
 
6ceefb7
bec85f3
 
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
bec85f3
 
 
 
6ceefb7
bec85f3
6ceefb7
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
bec85f3
6ceefb7
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
 
6ceefb7
 
 
bec85f3
6ceefb7
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
bec85f3
 
 
 
 
6ceefb7
 
 
 
 
 
 
bec85f3
 
 
6ceefb7
bec85f3
 
6ceefb7
 
 
 
 
 
 
 
 
 
bec85f3
 
 
6ceefb7
bec85f3
6ceefb7
 
 
bec85f3
6ceefb7
 
 
bec85f3
6ceefb7
 
bec85f3
6ceefb7
bec85f3
 
 
 
6ceefb7
 
 
bec85f3
 
 
6ceefb7
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec85f3
 
 
 
 
6ceefb7
bec85f3
6ceefb7
bec85f3
6ceefb7
bec85f3
 
 
 
 
 
 
 
 
6ceefb7
bec85f3
6ceefb7
bec85f3
 
6ceefb7
bec85f3
6ceefb7
bec85f3
 
6ceefb7
bec85f3
 
6ceefb7
 
 
 
bec85f3
 
 
 
 
 
6ceefb7
bec85f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6ceefb7
 
bec85f3
 
 
 
 
 
 
 
 
 
6ceefb7
 
 
 
bec85f3
 
 
 
 
6ceefb7
bec85f3
6ceefb7
bec85f3
6ceefb7
 
 
 
bec85f3
6ceefb7
 
 
 
bec85f3
6ceefb7
 
 
 
 
 
 
 
 
bec85f3
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
import json
import gradio as gr
from typing import Dict, Any
import os
from dataclasses import dataclass
import re
from llama_cpp import Llama
from huggingface_hub import hf_hub_download

@dataclass
class LocalModelConfig:
    """تنظیمات مدل محلی GGUF - Qwen2.5-32B"""
    repo_id: str = "Qwen/Qwen2.5-32B-Instruct-GGUF"
    filename: str = "qwen2.5-32b-instruct-q4_k_m.gguf"
    max_tokens: int = 8000
    temperature: float = 0.3
    top_p: float = 0.8
    n_ctx: int = 4096
    n_threads: int = 4  # کمتر برای Spaces
    n_gpu_layers: int = 50

class LocalCerebrasAnonymizer:
    """سیستم ناشناس‌سازی متون مالی فارسی با مدل محلی"""
    
    def __init__(self):
        self.config = LocalModelConfig()
        self.llm = None
        self.model_loaded = False
    
    def load_model(self) -> str:
        """بارگذاری مدل از HuggingFace"""
        try:
            print(f"🤖 درحال دانلود مدل از HuggingFace...")
            print(f"📦 Repo: {self.config.repo_id}")
            print(f"📄 Filename: {self.config.filename}")
            
            # دانلود مدل
            model_path = hf_hub_download(
                repo_id=self.config.repo_id,
                filename=self.config.filename,
                local_dir="./models",
                local_dir_use_symlinks=False
            )
            
            print(f"✅ مدل دانلود شد: {model_path}")
            print(f"🤖 درحال بارگذاری مدل...")
            
            self.llm = Llama(
                model_path=model_path,
                n_ctx=self.config.n_ctx,
                n_threads=self.config.n_threads,
                n_gpu_layers=self.config.n_gpu_layers,
                verbose=False
            )
            
            self.model_loaded = True
            print("✅ مدل با موفقیت بارگذاری شد\n")
            return "✅ مدل آماده است"
            
        except Exception as e:
            error_msg = f"❌ خطا: {str(e)}"
            print(error_msg)
            return error_msg
    
    def _create_system_prompt(self) -> str:
        """دستورالعمل سیستمی"""
        return """شما یک سیستم ناشناس‌سازی متون مالی فارسی هستید.

⚠️ CRITICAL: در پاسخ نهایی خود، فقط و فقط متن ناشناس‌سازی شده را برگردانید، بدون هیچ توضیح، تحلیل، یا تگ اضافی.

## قوانین اندیس‌گذاری:
1. **ترتیب پیوسته**: company-01, company-02, ... | person-01, person-02, ... | amount-01, amount-02, ... | percent-01, percent-02, ...
2. **ثبات**: اگر "همراه اول" → company-01 شد، در تمام متن همان باشد
3. **نام مستعار**: "فاما" = "فولاد مبارکه" → هر دو company-01

## انواع موجودیت:
- **company-XX**: شرکت‌ها، بانک‌ها، سازمان‌ها
- **person-XX**: نام و نام خانوادگی اشخاص  
- **amount-XX**: مبالغ - واحد را حفظ کن
- **percent-XX**: درصدها

## مثال:
ورودی: ایران خودرو در اسفند 1402 حدود 23 هزار میلیارد درآمد کسب کرد که 4.58 درصد افزایش داشت.
خروجی: company-01 در اسفند 1402 حدود amount-01 درآمد کسب کرد که percent-01 افزایش داشت.

⚠️ یادآوری: فقط متن ناشناس‌شده."""

    def anonymize_text(self, text: str) -> Dict[str, Any]:
        """ناشناس‌سازی متن"""
        if not self.model_loaded:
            return {"success": False, "error": "مدل بارگذاری نشده است"}
        
        if not text.strip():
            return {"success": False, "error": "متن ورودی خالی است"}
        
        try:
            messages = [
                {"role": "system", "content": self._create_system_prompt()},
                {"role": "user", "content": text}
            ]
            
            prompt = self._format_prompt(messages)
            
            print(f"⏳ پردازش متن... (طول: {len(text)} کاراکتر)")
            
            response = self.llm(
                prompt,
                max_tokens=self.config.max_tokens,
                temperature=self.config.temperature,
                top_p=self.config.top_p,
                stop=["</s>", "[/INST]", "### User:"]
            )
            
            content = response["choices"][0]["text"].strip()
            
            # پاک‌سازی
            content = self._remove_thinking_tags(content)
            content = self._clean_markdown(content)
            content = self._clean_explanations(content)
            content = content.strip()
            
            analysis = self._analyze_anonymized_text(content)
            
            return {
                "success": True,
                "anonymized_text": content,
                "entities": analysis["entities"],
                "statistics": analysis["statistics"],
                "detailed_analysis": analysis["detailed_analysis"],
                "quality_check": self._validate_anonymized_text(content)
            }
                
        except Exception as e:
            return {"success": False, "error": f"خطا: {str(e)}"}
    
    def _format_prompt(self, messages: list) -> str:
        """فرمت prompt برای Qwen2.5"""
        formatted = ""
        for message in messages:
            role = message["role"]
            content = message["content"]
            if role == "system":
                formatted += f"{content}\n\n"
            elif role == "user":
                formatted += f"[INST] {content} [/INST]\n"
            elif role == "assistant":
                formatted += f"{content}\n\n"
        return formatted
    
    def _remove_thinking_tags(self, content: str) -> str:
        content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
        content = re.sub(r'</?think>', '', content)
        return content.strip()
    
    def _clean_markdown(self, content: str) -> str:
        if "```" in content:
            lines = content.split('\n')
            clean_lines = []
            skip = False
            for line in lines:
                if line.strip().startswith('```'):
                    skip = not skip
                    continue
                if not skip:
                    clean_lines.append(line)
            content = '\n'.join(clean_lines)
        return content
    
    def _clean_explanations(self, content: str) -> str:
        lines = content.split('\n')
        clean_lines = []
        for line in lines:
            if any(word in line.lower() for word in 
                   ['okay', 'let me', 'here is', 'خروجی', 'نتیجه', 'پاسخ:', 'assistant', '[inst]']):
                continue
            clean_lines.append(line)
        return '\n'.join(clean_lines).strip()
    
    def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
        companies = re.findall(r'company-(\d+)', text)
        persons = re.findall(r'person-(\d+)', text)
        amounts = re.findall(r'amount-(\d+)', text)
        percents = re.findall(r'percent-(\d+)', text)
        
        statistics = {
            "company": len(set(companies)),
            "person": len(set(persons)),
            "amount": len(set(amounts)),
            "percent": len(set(percents)),
            "total": len(companies) + len(persons) + len(amounts) + len(percents)
        }
        
        entities = {
            "companies": sorted(list(set(companies)), key=lambda x: int(x)),
            "persons": sorted(list(set(persons)), key=lambda x: int(x)),
            "amounts": sorted(list(set(amounts)), key=lambda x: int(x)),
            "percents": sorted(list(set(percents)), key=lambda x: int(x))
        }
        
        detailed_analysis = {
            "preserved_dates": len(re.findall(r'\d{4}/\d{1,2}/\d{1,2}|\d{1,2}\s+\w+\s+\d{4}', text)),
            "financial_indicators": len(re.findall(r'\b(EPS|P/E|ARPU|NPL|ROE|ROA)\b', text)),
            "units_preserved": len(re.findall(r'(میلیارد|میلیون|هزار|تومان|ریال|درهم|دلار)', text))
        }
        
        return {
            "statistics": statistics,
            "entities": entities,
            "detailed_analysis": detailed_analysis
        }
    
    def _validate_anonymized_text(self, text: str) -> Dict[str, Any]:
        companies = re.findall(r'company-(\d+)', text)
        persons = re.findall(r'person-(\d+)', text)
        amounts = re.findall(r'amount-(\d+)', text)
        percents = re.findall(r'percent-(\d+)', text)
        
        validation_issues = []
        
        for entity_type, indices in [("company", companies), ("person", persons), 
                                      ("amount", amounts), ("percent", percents)]:
            if indices:
                unique_indices = sorted(list(set([int(x) for x in indices])))
                if unique_indices[0] != 1:
                    validation_issues.append(f"⚠️ {entity_type} از 01 شروع نشده")
                
                expected = list(range(1, len(unique_indices) + 1))
                if unique_indices != expected:
                    validation_issues.append(f"⚠️ {entity_type} پیوسته نیست")
        
        return {
            "is_valid": len(validation_issues) == 0,
            "issues": validation_issues,
            "entity_counts": {
                "company": len(set(companies)),
                "person": len(set(persons)),
                "amount": len(set(amounts)),
                "percent": len(set(percents))
            }
        }

# ========== رابط کاربری ==========

anonymizer = LocalCerebrasAnonymizer()

def create_interface():
    custom_css = """
    .gradio-container {
        font-family: 'Tahoma', 'Arial', sans-serif !important;
        direction: rtl;
        max-width: 1400px;
        margin: 0 auto;
    }
    .info-box {
        background-color: #e3f2fd;
        border: 2px solid #2196F3;
        border-radius: 12px;
        padding: 15px;
        color: #0d47a1;
        margin: 10px 0;
    }
    .local-box {
        background-color: #e8f5e9;
        border: 2px solid #4caf50;
        border-radius: 12px;
        padding: 15px;
        color: #1b5e20;
        margin: 10px 0;
    }
    .result-box {
        background-color: #f8f9fa;
        border: 2px solid #e9ecef;
        border-radius: 12px;
        padding: 20px;
    }
    """
    
    with gr.Blocks(css=custom_css, title="ناشناس‌ساز Qwen2.5", theme=gr.themes.Soft()) as interface:
        
        gr.Markdown("""
        # 🔒 سیستم ناشناس‌سازی متون مالی فارسی
        ### 🚀 Qwen 2.5-32B (HuggingFace Spaces)
        """)
        
        gr.Markdown("""
        <div class="info-box">
        📊 <strong>مدل:</strong> Qwen2.5-32B-Instruct-Q4_K_M<br>
        🌐 <strong>منبع:</strong> HuggingFace Hub<br>
        💾 <strong>حجم:</strong> ~20 GB (Q4 quantization)<br>
        ⚡ <strong>سرعت:</strong> بستگی به GPU Spaces دارد
        </div>
        """)
        
        status_box = gr.Textbox(label="📋 وضعیت", interactive=False, value="⏳ درحال بارگذاری مدل...")
        
        load_btn = gr.Button("🤖 بارگذاری مدل", variant="primary", size="lg")
        
        with gr.Row(visible=False) as input_section:
            with gr.Column(scale=1):
                input_text = gr.Textbox(
                    label="📝 متن ورودی",
                    placeholder="متن خود را اینجا وارد کنید...",
                    lines=12,
                    max_lines=25
                )
                
                with gr.Row():
                    anonymize_btn = gr.Button("🔒 ناشناس‌سازی", variant="primary", size="lg")
                    clear_btn = gr.Button("🗑️ پاک کردن", variant="secondary")
            
            with gr.Column(scale=1):
                output_text = gr.Textbox(
                    label="🎯 متن ناشناس‌سازی شده",
                    lines=12,
                    max_lines=25,
                    elem_classes=["result-box"]
                )
        
        with gr.Row(visible=False) as output_section:
            with gr.Column():
                statistics_output = gr.Markdown(label="📊 آمار")
            with gr.Column():
                quality_output = gr.Markdown(label="✅ کیفیت")
        
        with gr.Row(visible=False) as output_section2:
            entities_output = gr.Markdown(label="🏷️ موجودیت‌ها")
            detailed_output = gr.Markdown(label="🔍 تحلیل")
        
        def load_model_action():
            """بارگذاری مدل"""
            msg = anonymizer.load_model()
            return (
                gr.Textbox(value=msg),
                gr.Row(visible=True),
                gr.Row(visible=True),
                gr.Row(visible=True)
            )
        
        def process_text(text):
            """پردازش متن"""
            if not text.strip():
                return ("", "❌ متن خالی است", "", "", "", "")
            
            result = anonymizer.anonymize_text(text)
            
            if not result["success"]:
                return ("", f"❌ {result['error']}", "", "", "", "")
            
            stats = result.get("statistics", {})
            stats_md = f"""📊 **آمار:**
🏢 شرکت: {stats.get('company', 0)}
👤 اشخاص: {stats.get('person', 0)}
💰 مبالغ: {stats.get('amount', 0)}
📊 درصدها: {stats.get('percent', 0)}
🔢 کل: {stats.get('total', 0)}"""
            
            quality = result.get("quality_check", {})
            quality_md = f"""✅ **کنترل کیفیت:**

{'✅ موفق' if quality.get('is_valid') else '❌ مشکل'}
"""
            if quality.get("issues"):
                quality_md += "\n**مشکلات:**\n"
                for issue in quality["issues"]:
                    quality_md += f"• {issue}\n"
            
            entities = result.get("entities", {})
            entities_md = "🏷️ **موجودیت‌ها:**\n"
            if entities.get("companies"):
                entities_md += f"\n🏢 company-{', company-'.join(entities['companies'])}"
            if entities.get("persons"):
                entities_md += f"\n👤 person-{', person-'.join(entities['persons'])}"
            if entities.get("amounts"):
                entities_md += f"\n💰 amount-{', amount-'.join(entities['amounts'])}"
            if entities.get("percents"):
                entities_md += f"\n📊 percent-{', percent-'.join(entities['percents'])}"
            
            detailed = result.get("detailed_analysis", {})
            detailed_md = f"""🔍 **تحلیل:**
📅 تاریخ: {detailed.get('preserved_dates', 0)}
📈 شاخص: {detailed.get('financial_indicators', 0)}
📏 واحد: {detailed.get('units_preserved', 0)}"""
            
            return (
                result["anonymized_text"],
                stats_md,
                quality_md,
                entities_md,
                detailed_md,
                "✅ موفق"
            )
        
        def clear_all():
            return "", "", "", "", "", ""
        
        load_btn.click(
            fn=load_model_action,
            outputs=[status_box, input_section, output_section, output_section2]
        )
        
        anonymize_btn.click(
            fn=process_text,
            inputs=[input_text],
            outputs=[output_text, statistics_output, quality_output, entities_output, detailed_output, status_box]
        )
        
        clear_btn.click(
            fn=clear_all,
            outputs=[input_text, output_text, statistics_output, quality_output, entities_output, detailed_output]
        )
        
        gr.Examples(
            examples=[
                ["ایران خودرو در اسفندماه حدود 23 هزار میلیارد تومان درآمد کسب کرد که 4.58 درصد افزایش داشت."],
                ["مجمع پتروشیمی برگزار شد. وانیا نیک تدبیر را بازرس انتخاب کردند."],
            ],
            inputs=input_text,
            label="📚 مثال‌ها"
        )
        
        return interface

if __name__ == "__main__":
    interface = create_interface()
    interface.launch()