File size: 13,222 Bytes
8bd13e5
 
 
6b8a8b0
eb5a6f1
50e9d08
8bd13e5
a7a30dc
5a37321
e770230
8bd13e5
66d9fd6
8bd13e5
 
 
50e9d08
 
 
 
 
 
 
 
 
 
 
 
63ad1a9
 
 
 
 
 
 
50e9d08
 
 
63ad1a9
50e9d08
63ad1a9
50e9d08
 
63ad1a9
 
50e9d08
 
63ad1a9
 
50e9d08
 
 
63ad1a9
 
 
 
 
5756c34
 
 
63ad1a9
5756c34
 
63ad1a9
 
 
50e9d08
 
 
 
63ad1a9
50e9d08
 
63ad1a9
 
4eb2d7a
 
50e9d08
 
 
 
 
 
 
 
 
 
5a37321
8bd13e5
5a37321
8bd13e5
 
 
 
a7a30dc
8bd13e5
 
 
 
 
 
a7a30dc
8bd13e5
50e9d08
 
 
 
0e31e2b
50e9d08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9415c0
50e9d08
 
 
 
 
 
3c10f1d
50e9d08
e770230
50e9d08
a65d937
 
50e9d08
 
 
 
a65d937
 
50e9d08
 
 
 
 
 
 
 
 
63ad1a9
50e9d08
a65d937
 
 
 
 
 
 
63ad1a9
 
 
 
 
a65d937
50e9d08
 
 
 
 
763239c
e770230
50e9d08
 
5a37321
e770230
50e9d08
715f0d7
5756c34
63ad1a9
5756c34
 
 
 
63ad1a9
4eb2d7a
 
63ad1a9
 
 
 
 
5756c34
63ad1a9
4eb2d7a
63ad1a9
 
 
 
5756c34
63ad1a9
b2a0df4
4eb2d7a
63ad1a9
 
 
 
b2a0df4
715f0d7
f79ee9b
50e9d08
7f51941
50e9d08
63ad1a9
 
 
 
 
 
 
 
 
5756c34
63ad1a9
 
 
5756c34
8bbcc70
50e9d08
 
 
 
715f0d7
50e9d08
7ed82cc
8bbcc70
50e9d08
 
b2a0df4
50e9d08
 
b2a0df4
50e9d08
 
8bbcc70
50e9d08
 
 
 
 
 
 
 
 
8bbcc70
 
 
715f0d7
8bbcc70
 
 
8bd13e5
 
3c10f1d
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
import os
import time
import asyncio
import urllib.parse
import re
import json
import uvicorn
from fastapi import FastAPI, Form, File, UploadFile, HTTPException
from fastapi.responses import FileResponse, StreamingResponse
from playwright.async_api import async_playwright

app = FastAPI()
UPLOAD_DIR = "temp_uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

def clean_extracted_text(raw_text: str, user_message: str) -> str:
    if not raw_text:
        return ""
        
    lines = raw_text.split('\n')
    cleaned_lines = []
    
    disclaimer_triggers = [
        "هوشواره", "اشتباه کند", "دوباره‌سنجی", "دوباره سنجی", 
        "پیش‌نمایش", "Generative AI", "experimental"
    ]
    
    # قیچی‌های قطع کامل: اگر به این کلمات برسیم، کلاً خواندن را متوقف می‌کنیم تا سایت‌های فروشگاهی نشان داده نشوند
    cutoff_triggers = [
        "visual matches", "نتایج مشابه", "تصاویر مشابه", "جستجوهای مرتبط",
        "جستجوی مرتبط", "یافتن منبع تصویر", "مطابقت دقیق", "نمایش همه",
        "همچنین ببینید", "search", "google lens"
    ]
    
    skip_keywords = [
        "تصاویر", "ویدیوها", "ویدئوها", "اخبار", "نقشه‌ها", "خرید کردن", "کتاب‌ها", "مالی", 
        "حالت موضوع‌محور", "ورود", "جستجو", "تنظیمات", "ابزارها", "صفحه اصلی", "همه", "خرید", 
        "پروازها", "بیشتر بدانید", "در پاسخ‌های", "بازخورد", "درباره این نتیجه", 
        "گزارش این", "Feedback", "About this result", "حالت هوشواره‌ای", "اگر امکان دارد آن را به فارسی ایمیل کنید",
        "متن پیدا شد", "ترجمه", "مشاهده موارد مشابه", "جستجوی تصویر"
    ]
    
    spam_domains = ["etsy", "pinterest", "dreamstime", "amazon", "ebay", "shutterstock", "istock", "redbubble", "aliexpress"]
    
    for line in lines:
        line_str = re.sub(r'[\u200b-\u200d\u200f\u202a-\u202e]', '', line).strip()
        line_lower = line_str.lower()
        
        if not line_str:
            continue
            
        # ۱. اگر به بخش سایت‌های مشابه رسیدیم، کلاً بقیه متن را حذف کن
        if any(trigger in line_lower for trigger in cutoff_triggers) or any(trigger in line_lower for trigger in disclaimer_triggers):
            break
            
        # ۲. فیلتر کردن کدهای اضافی CSS
        if "var(--" in line_str or "-webkit-" in line_str or "display:" in line_str or line_str.startswith("."):
            continue
            
        if re.search(r'^\d+\+?$', line_str):  
            continue
            
        # ۳. فیلتر کردن نام سایت‌های مزاحم مثل Etsy
        if re.search(r'^[a-zA-Z0-9-]+\.[a-zA-Z]{2,4}$', line_str, re.IGNORECASE) or any(domain in line_lower for domain in spam_domains):  
            continue
            
        if re.search(r'^\d+\s+سایت$', line_str) or re.search(r'^\d+\s+site', line_str, re.IGNORECASE):
            continue
        
        if any(keyword == line_lower for keyword in skip_keywords):
            continue
            
        # ۴. حذف خطوط انگلیسی که مربوط به عنوان محصولات فروشگاهی هستند
        if line_str.endswith("...") and not re.search(r'[\u0600-\u06FF]', line_str):
            continue
            
        cleaned_lines.append(line_str)
        
    if cleaned_lines:
        first_line = re.sub(r'[؟\?]', '', cleaned_lines[0].strip().lower())
        msg_clean = re.sub(r'[؟\?]', '', user_message.strip().lower())
        if first_line == msg_clean or msg_clean in first_line or first_line in msg_clean:
            cleaned_lines.pop(0)
            
    return "\n".join(cleaned_lines).strip()

@app.get("/")
async def chat_interface():
    return FileResponse("index.html")

@app.post("/api/chat")
async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
    saved_image_path = None
    
    if image:
        try:
            saved_image_path = os.path.join(UPLOAD_DIR, f"{int(time.time())}_{image.filename}")
            with open(saved_image_path, "wb") as buffer:
                buffer.write(await image.read())
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"خطا در ذخیره‌سازی تصویر: {str(e)}")

    async def generate_response():
        async with async_playwright() as p:
            try:
                is_multimodal = bool(saved_image_path and os.path.exists(saved_image_path))
                
                if is_multimodal:
                    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
                    viewport = {"width": 1280, "height": 720}
                else:
                    user_agent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36"
                    viewport = {"width": 412, "height": 915}

                browser = await p.chromium.launch(
                    headless=True,
                    args=[
                        "--no-sandbox", 
                        "--disable-setuid-sandbox", 
                        "--disable-dev-shm-usage",
                        "--disable-blink-features=AutomationControlled"
                    ]
                )
                
                context = await browser.new_context(
                    user_agent=user_agent,
                    viewport=viewport,
                    locale="fa-IR",
                    timezone_id="Asia/Tehran"
                )
                
                page = await context.new_page()
                
                if is_multimodal:
                    await page.goto("https://images.google.com/", wait_until="networkidle", timeout=45000)
                    await page.wait_for_timeout(2000)
                    
                    uploaded = False
                    try:
                        camera_btn = page.locator('div[aria-label="Search by image"], div[aria-label="جستجو با تصویر"], [aria-label*="camera" i], [aria-label*="دوربین"]').first
                        await camera_btn.click(timeout=10000)
                        await page.wait_for_timeout(1500)
                        
                        file_input = page.locator("input[type='file']").first
                        await file_input.wait_for(state="attached", timeout=5000)
                        await file_input.set_input_files(saved_image_path)
                        uploaded = True
                    except Exception:
                        pass

                    if uploaded:
                        await page.wait_for_timeout(6000)
                        try:
                            inputs = await page.locator("textarea, input[type='text'], [contenteditable='true']").all()
                            for inp in inputs:
                                if await inp.is_visible() and not await inp.is_disabled():
                                    await inp.focus()
                                    await inp.fill(message)
                                    await page.wait_for_timeout(500)
                                    await page.keyboard.press("Enter")
                                    
                                    # ---> مکث طلایی (Golden Delay) <---
                                    # اینجا 4.5 ثانیه صبر می‌کنیم تا گوگل لنز از سایت‌های فروشگاهی گذر کند
                                    # و هوش مصنوعی جوابش را کامل تولید کند. با این کار دیگر پیام اولیه ناقص نشان داده نمی‌شود.
                                    await page.wait_for_timeout(4500)
                                    break
                        except Exception:
                            pass
                else:
                    encoded_query = urllib.parse.quote_plus(message)
                    url = f"https://www.google.com/search?q={encoded_query}&udm=50"
                    await page.goto(url, wait_until="domcontentloaded", timeout=45000)
                
                previous_text = ""
                unchanged_count = 0
                max_iterations = 300 
                
                for _ in range(max_iterations):
                    raw_ai_text = await page.evaluate("""() => {
                        function isGoodText(txt) {
                            if (!txt || txt.length < 25) return false;
                            if (txt.includes('var(--') || txt.includes('-webkit-')) return false;
                            return true;
                        }

                        // اولویت با خواندن دقیق کادر هوش مصنوعی
                        const aiSelectors = [
                            'div[aria-label*="AI Overview"]',
                            'div[aria-label*="AI-generated"]',
                            'div[data-subtree="aimc"]',
                            'div[data-subtree="mfc"]',
                            '.aah4tc',
                            'div[data-m="0"] div[dir="rtl"]'
                        ];
                        
                        for (const sel of aiSelectors) {
                            try {
                                const els = document.querySelectorAll(sel);
                                for (const el of els) {
                                    if (isGoodText(el.innerText)) return el.innerText.trim();
                                }
                            } catch(e) {}
                        }

                        // کادر اصلی اگر کادرهای بالا پیدا نشد
                        const mainLens = document.querySelector('div[data-m="0"]');
                        if (mainLens && isGoodText(mainLens.innerText)) return mainLens.innerText.trim();
                        
                        return "";
                    }""")
                    
                    cleaned_text = clean_extracted_text(raw_ai_text, message)
                    
                    if cleaned_text and cleaned_text != previous_text:
                        # جلوگیری از پرش متن: اگر متن قبلی خیلی کامل بود و این متن جدید کوتاه است، نادیده‌اش بگیر
                        if previous_text and len(cleaned_text) < 50 and len(previous_text) > 100:
                            continue

                        is_replacement = not previous_text or not cleaned_text.startswith(previous_text[:len(previous_text)//2 + 1])
                        
                        if is_replacement or len(cleaned_text) > len(previous_text) + 20:
                            start_idx = 0 if is_replacement else len(previous_text)
                            step = max(3, (len(cleaned_text) - start_idx) // 15)
                            
                            for i in range(start_idx + step, len(cleaned_text), step):
                                yield f"data: {json.dumps({'text': cleaned_text[:i]}, ensure_ascii=False)}\n\n"
                                await asyncio.sleep(0.04) 
                        
                        yield f"data: {json.dumps({'text': cleaned_text}, ensure_ascii=False)}\n\n"
                        previous_text = cleaned_text
                        unchanged_count = 0
                    elif cleaned_text and cleaned_text == previous_text:
                        unchanged_count += 1
                        if unchanged_count >= 30:
                            break
                    
                    await asyncio.sleep(0.1)

                if not previous_text:
                    fallback_text = await page.evaluate("() => document.body.innerText")
                    clean_fb = clean_extracted_text(fallback_text, message)
                    if clean_fb:
                        yield f"data: {json.dumps({'text': clean_fb[:2000] + '...'}, ensure_ascii=False)}\n\n"

            except Exception as e:
                yield f"data: {json.dumps({'text': f'خطا: {str(e)}'}, ensure_ascii=False)}\n\n"
            finally:
                if 'browser' in locals():
                    await browser.close()
                if saved_image_path and os.path.exists(saved_image_path):
                    try:
                        os.remove(saved_image_path)
                    except:
                        pass

    headers = {
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "X-Accel-Buffering": "no"
    }
    
    return StreamingResponse(generate_response(), media_type="text/event-stream", headers=headers)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)