Opera8 commited on
Commit
8bd13e5
·
verified ·
1 Parent(s): 632f40e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +378 -47
app.py CHANGED
@@ -1,54 +1,385 @@
1
- # یک پروکسی لینک مستقیم برای استفاده در برنامه‌ها و ربات‌های دیگر
2
- @app.get("/proxy")
3
- async def proxy_endpoint(q: str):
4
- if not q.strip():
5
- return {"error": "عبارت جستجو نمی‌تواند خالی باشد"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- global global_browser
8
- url = f"https://www.google.com/search?q={q}&udm=50"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- context = await global_browser.new_context(
11
- user_agent="Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
12
- viewport={"width": 412, "height": 915},
13
- locale="fa-IR", timezone_id="Asia/Tehran"
14
- )
15
- page = await context.new_page()
16
 
17
- try:
18
- await page.goto(url, wait_until="commit", timeout=20000)
19
- await page.wait_for_timeout(500)
 
 
 
 
 
20
 
21
- # شبیه‌سازی زدن اینتر
22
- input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']")
23
- if input_box:
24
- await input_box.focus()
25
- await input_box.press("Enter")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- # چک کردن داینامیک پایان تایپ گوگل
28
- full_text = ""
29
- for _ in range(25):
30
- await page.wait_for_timeout(400)
31
- full_text = await page.evaluate("() => document.body.innerText")
32
- if "هوشواره ممکن است اشتباه" in full_text or "بیشتر بدانید" in full_text:
33
- break
 
34
 
35
- # تصفیه و استخراج متن خالص
36
- lines = full_text.split('\n')
37
- cleaned_lines = []
38
- for line in lines:
39
- line_str = line.strip()
40
- if not line_str or any(j == line_str for j in ["تصاویر", "ویدیوها", "اخبار", "نقشه‌ها", "خرید کردن", "کتاب‌ها", "مالی", "حالت موضوع‌محور", "ورود", "همه", "ارسال"]):
41
- continue
42
- if "هوشواره ممکن است اشتباه" in line_str or "بیشتر بدانید" in line_str:
43
- break
44
- if line_str == q:
45
- cleaned_lines = []
46
- continue
47
- cleaned_lines.append(line_str)
 
 
48
 
49
- await context.close()
50
- return HTMLResponse(content="\n".join(cleaned_lines).strip(), status_code=200)
51
-
52
- except Exception as e:
53
- await context.close()
54
- return HTMLResponse(content=f"Error: {str(e)}", status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import asyncio
4
+ import uvicorn
5
+ from fastapi import FastAPI, Form, File, UploadFile
6
+ from fastapi.responses import HTMLResponse, StreamingResponse
7
+ from playwright.async_api import async_playwright
8
+ from contextlib import asynccontextmanager
9
+
10
+ UPLOAD_DIR = "temp_uploads"
11
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
12
+
13
+ global_browser = None
14
+ pw_instance = None
15
+
16
+ @asynccontextmanager
17
+ async def lifespan(app: FastAPI):
18
+ global global_browser, pw_instance
19
+ print("⚡ در حال راه‌اندازی مرورگر سراسری کروم...")
20
+ pw_instance = await async_playwright().start()
21
+ global_browser = await pw_instance.chromium.launch(
22
+ headless=True,
23
+ args=[
24
+ "--no-sandbox",
25
+ "--disable-setuid-sandbox",
26
+ "--disable-dev-shm-usage",
27
+ "--disable-blink-features=AutomationControlled"
28
+ ]
29
+ )
30
+ print("🚀 مرورگر سراسری با موفقیت لود شد.")
31
+ yield
32
+ print("🛑 در حال بستن مرورگر سراسری...")
33
+ if global_browser:
34
+ await global_browser.close()
35
+ if pw_instance:
36
+ await pw_instance.stop()
37
+
38
+ app = FastAPI(lifespan=lifespan)
39
+
40
+ # رابط کاربری با قابلیت خواندن استریم باینری پاسخ‌ها با استفاده از ReadableStream فرچ
41
+ HTML_CHAT_INTERFACE = """
42
+ <!DOCTYPE html>
43
+ <html lang="fa" dir="rtl">
44
+ <head>
45
+ <meta charset="UTF-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>Google AI Free Chatbot + Streaming</title>
48
+ <style>
49
+ :root {
50
+ --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
51
+ --glass-bg: rgba(30, 41, 59, 0.7);
52
+ --glass-border: rgba(255, 255, 255, 0.08);
53
+ --text-main: #f8fafc;
54
+ --accent-color: #38bdf8;
55
+ --user-bubble: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
56
+ --ai-bubble: rgba(255, 255, 255, 0.06);
57
+ }
58
+
59
+ body {
60
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Tahoma, sans-serif;
61
+ margin: 0; padding: 0; background: var(--bg-gradient); color: var(--text-main);
62
+ height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden;
63
+ }
64
+
65
+ .chat-container {
66
+ width: 100%; max-width: 850px; height: 85vh; background: var(--glass-bg);
67
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
68
+ border: 1px solid var(--glass-border); border-radius: 24px;
69
+ display: flex; flex-direction: column; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); margin: 20px;
70
+ }
71
+
72
+ .chat-header { padding: 20px 24px; border-bottom: 1px solid var(--glass-border); display: flex; align-items: center; gap: 12px; }
73
+ .chat-header .status-dot { width: 10px; height: 10px; background: #4ade80; border-radius: 50%; box-shadow: 0 0 10px #4ade80; }
74
+ .chat-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: var(--accent-color); }
75
+ .messages-box { flex: 1; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
76
+ .messages-box::-webkit-scrollbar { width: 6px; }
77
+ .messages-box::-webkit-scrollbar-track { background: transparent; }
78
+ .messages-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 10px; }
79
+
80
+ .message { max-width: 75%; padding: 14px 18px; border-radius: 18px; font-size: 15px; line-height: 1.6; word-wrap: break-word; animation: fadeIn 0.3s ease forwards; }
81
+ .message.user { background: var(--user-bubble); align-self: flex-start; border-bottom-right-radius: 4px; box-shadow: 0 4px 12px rgba(2, 132, 199, 0.2); }
82
+ .message.ai { background: var(--ai-bubble); border: 1px solid var(--glass-border); align-self: flex-end; border-bottom-left-radius: 4px; white-space: pre-wrap; }
83
+
84
+ .input-wrapper { border-top: 1px solid var(--glass-border); background: rgba(15, 23, 42, 0.4); border-bottom-left-radius: 24px; border-bottom-right-radius: 24px; padding: 15px 24px; display: flex; flex-direction: column; gap: 10px; }
85
+ .preview-container { display: none; align-items: center; gap: 10px; background: rgba(255, 255, 255, 0.05); padding: 8px 12px; border-radius: 10px; width: fit-content; border: 1px solid var(--glass-border); }
86
+ .preview-container img { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; }
87
+ .preview-container .remove-btn { color: #ef4444; cursor: pointer; font-weight: bold; font-size: 14px; }
88
+
89
+ .input-area { display: flex; gap: 12px; align-items: center; }
90
+ .input-area input[type="text"] { flex: 1; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--glass-border); padding: 14px 20px; border-radius: 14px; color: white; font-size: 15px; outline: none; transition: all 0.2s; }
91
+ .input-area input[type="text"]:focus { border-color: var(--accent-color); background: rgba(255, 255, 255, 0.08); box-shadow: 0 0 12px rgba(56, 189, 248, 0.15); }
92
 
93
+ .upload-label { background: rgba(255, 255, 255, 0.05); border: 1px solid var(--glass-border); padding: 12px; border-radius: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
94
+ .upload-label:hover { background: rgba(255, 255, 255, 0.1); border-color: var(--accent-color); }
95
+ .upload-label svg { width: 22px; height: 22px; fill: var(--text-main); }
96
+
97
+ .input-area button { background: var(--accent-color); color: #0f172a; border: none; padding: 14px 24px; border-radius: 14px; font-size: 15px; font-weight: bold; cursor: pointer; transition: all 0.2s; }
98
+ .input-area button:hover { background: #7dd3fc; transform: translateY(-1px); }
99
+ .input-area button:disabled { background: #64748b; color: #1e293b; cursor: not-allowed; transform: none; }
100
+
101
+ .typing-indicator { display: flex; gap: 5px; padding: 12px 18px; background: var(--ai-bubble); border-radius: 12px; align-self: flex-end; border: 1px solid var(--glass-border); }
102
+ .typing-indicator span { width: 8px; height: 8px; background: var(--accent-color); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
103
+ .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
104
+ .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
105
+
106
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
107
+ @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } }
108
+ </style>
109
+ </head>
110
+ <body>
111
+
112
+ <div class="chat-container">
113
+ <div class="chat-header">
114
+ <div class="status-dot"></div>
115
+ <h2>دستیار مالتی‌مدال گوگل (متن + تصویر زنده و سریع)</h2>
116
+ </div>
117
+ <div class="messages-box" id="messagesBox">
118
+ <div class="message ai">سلام! من متصل به هوشواره گوگل هستم. کلمات را به صورت کلمه به کلمه و زنده تایپ می‌کنم. چطور می‌توانم کمکتان کنم؟</div>
119
+ </div>
120
+
121
+ <div class="input-wrapper">
122
+ <div class="preview-container" id="previewContainer">
123
+ <img src="" id="imagePreview">
124
+ <span class="remove-btn" onclick="removeSelectedImage()">✕ حذف</span>
125
+ </div>
126
+
127
+ <div class="input-area">
128
+ <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="previewImage(event)">
129
+ <label for="fileInput" class="upload-label" title="آپلود تصویر">
130
+ <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.66 1.34 3 3 3s3-1.34 3-3V5c0-2.48-2.02-4.5-4.5-4.5S7 2.52 7 5v12.5c0 3.59 2.91 6.5 6.5 6.5s6.5-2.91 6.5-6V6h-1.5z"/></svg>
131
+ </label>
132
+
133
+ <input type="text" id="userInput" placeholder="پیام یا توضیح مربوط به تصویر را بنویسید..." onkeypress="handleKeyPress(event)">
134
+ <button id="sendBtn" onclick="sendMessage()">ارسال</button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <script>
140
+ const messagesBox = document.getElementById('messagesBox');
141
+ const userInput = document.getElementById('userInput');
142
+ const fileInput = document.getElementById('fileInput');
143
+ const sendBtn = document.getElementById('sendBtn');
144
+ const previewContainer = document.getElementById('previewContainer');
145
+ const imagePreview = document.getElementById('imagePreview');
146
+
147
+ function handleKeyPress(e) { if (e.key === 'Enter') sendMessage(); }
148
+
149
+ function previewImage(event) {
150
+ const file = event.target.files[0];
151
+ if (file) {
152
+ const reader = new FileReader();
153
+ reader.onload = function(e) { imagePreview.src = e.target.result; previewContainer.style.display = 'flex'; }
154
+ reader.readAsDataURL(file);
155
+ }
156
+ }
157
+
158
+ function removeSelectedImage() { fileInput.value = ''; previewContainer.style.display = 'none'; imagePreview.src = ''; }
159
+
160
+ async function sendMessage() {
161
+ const text = userInput.value.trim();
162
+ const file = fileInput.files[0];
163
+ if (!text && !file) return;
164
+
165
+ if (file) appendImageMessage(imagePreview.src, 'user');
166
+ if (text) appendMessage(text, 'user');
167
+
168
+ userInput.value = '';
169
+ removeSelectedImage();
170
+ setLoading(true);
171
+
172
+ const formData = new FormData();
173
+ formData.append('message', text || "این تصویر را تحلیل کن");
174
+ if (file) formData.append('image', file);
175
+
176
+ // ایجاد حباب خالی هوش مصنوعی برای آماده‌سازی فرآیند تایپ زنده
177
+ const aiMessageDiv = document.createElement('div');
178
+ aiMessageDiv.classList.add('message', 'ai');
179
+ messagesBox.appendChild(aiMessageDiv);
180
+
181
+ try {
182
+ const response = await fetch('/api/chat', { method: 'POST', body: formData });
183
+
184
+ if (!response.ok) {
185
+ aiMessageDiv.innerText = "خطا در برقراری ارتباط با سرور گوگل.";
186
+ setLoading(false);
187
+ return;
188
+ }
189
+
190
+ // خواندن باینری استریم کلمه به کلمه خروجی پایتون
191
+ const reader = response.body.getReader();
192
+ const decoder = new TextDecoder("utf-8");
193
+
194
+ // پنهان کردن انیمیشن لودینگ به محض شروع اولین کلمه دریافتی
195
+ let firstChunk = true;
196
+
197
+ while (true) {
198
+ const { value, done } = await reader.read();
199
+ if (done) break;
200
+
201
+ const chunk = decoder.decode(value, { stream: true });
202
+ if (chunk) {
203
+ if (firstChunk) {
204
+ // حذف لودینگ سه نقطه‌ای
205
+ const indicator = document.getElementById('typingIndicator');
206
+ if (indicator) indicator.remove();
207
+ firstChunk = false;
208
+ }
209
+ aiMessageDiv.innerText += chunk;
210
+ messagesBox.scrollTop = messagesBox.scrollHeight;
211
+ }
212
+ }
213
+ } catch (error) {
214
+ aiMessageDiv.innerText = "خطای شبکه: دریافت استریم با شکست مواجه شد.";
215
+ } finally {
216
+ setLoading(false);
217
+ }
218
+ }
219
+
220
+ function appendMessage(text, sender) {
221
+ const msgDiv = document.createElement('div'); msgDiv.classList.add('message', sender);
222
+ msgDiv.innerText = text; messagesBox.appendChild(msgDiv); messagesBox.scrollTop = messagesBox.scrollHeight;
223
+ }
224
+
225
+ function appendImageMessage(src, sender) {
226
+ const msgDiv = document.createElement('div'); msgDiv.classList.add('message', sender); msgDiv.style.padding = '8px';
227
+ const img = document.createElement('img'); img.src = src; img.style.maxWidth = '200px'; img.style.maxHeight = '200px'; img.style.borderRadius = '12px'; img.style.display = 'block';
228
+ msgDiv.appendChild(img); messagesBox.appendChild(msgDiv); messagesBox.scrollTop = messagesBox.scrollHeight;
229
+ }
230
+
231
+ function setLoading(isLoading) {
232
+ if (isLoading) {
233
+ userInput.disabled = true; sendBtn.disabled = true;
234
+ const indicator = document.createElement('div'); indicator.classList.add('typing-indicator'); indicator.id = 'typingIndicator';
235
+ indicator.innerHTML = '<span></span><span></span><span></span>'; messagesBox.appendChild(indicator); messagesBox.scrollTop = messagesBox.scrollHeight;
236
+ } else {
237
+ userInput.disabled = false; sendBtn.disabled = false;
238
+ const indicator = document.getElementById('typingIndicator'); if (indicator) indicator.remove(); userInput.focus();
239
+ }
240
+ }
241
+ </script>
242
+ </body>
243
+ </html>
244
+ """
245
+
246
+ # الگوریتم مهندسی معکوس برای استخراج و تصفیه متن خالص پاسخ هوش مصنوعی
247
+ def clean_text(full_text: str, message: str) -> str:
248
+ lines = full_text.split('\n')
249
+ cleaned_lines = []
250
 
251
+ # واژه‌های کاربری منوهای ناوبری که نباید به هیچ وجه نشت پیدا کنند
252
+ global_junk = ["تصاویر", "ویدیوها", "اخبار", "نقشه‌ها", "خرید کردن", "کتاب‌ها", "مالی", "حالت موضوع‌محور", "ورود", "همه", "ویدئوها", "ارسال", "پاسخ «حالت هوشواره‌ای» آماده است", "All items removed", message]
 
 
 
 
253
 
254
+ for line in lines:
255
+ line_str = line.strip()
256
+ if not line_str or line_str in global_junk:
257
+ continue
258
+ # به محض رسیدن به لایه فوتر سلب مسئولیت، کلاً خواندن را قطع کن
259
+ if "هوشواره ممکن است اشتباه" in line_str or "در پاسخ‌های" in line_str or "بیشتر بدانید" in line_str:
260
+ break
261
+ cleaned_lines.append(line_str)
262
 
263
+ return "\n".join(cleaned_lines).strip()
264
+
265
+ @app.get("/", response_class=HTMLResponse)
266
+ async def chat_interface():
267
+ return HTML_CHAT_INTERFACE
268
+
269
+ @app.post("/api/chat")
270
+ async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
271
+ global global_browser
272
+ if not global_browser:
273
+ return StreamingResponse(iter(["خطا: وب سرور کروم فعال نیست."]), media_type="text/plain")
274
+
275
+ saved_image_path = None
276
+ if image:
277
+ try:
278
+ saved_image_path = os.path.join(UPLOAD_DIR, f"{int(time.time())}_{image.filename}")
279
+ with open(saved_image_path, "wb") as buffer:
280
+ buffer.write(await image.read())
281
+ except Exception as e:
282
+ return StreamingResponse(iter([f"خطا در ذخیره تصویر روی داکر: {str(e)}"]), media_type="text/plain")
283
+
284
+ # ساخت سشن ژنراتور استریمینگ زنده کلمات برای پاسخ در لحظه
285
+ async def response_generator():
286
+ # استفاده از domcontentloaded برای بالا بردن سرعت اولیه رندر فرم‌ها بدون معطلی تصاویر حجیم
287
+ context = await global_browser.new_context(
288
+ user_agent="Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
289
+ viewport={"width": 412, "height": 915},
290
+ locale="fa-IR", timezone_id="Asia/Tehran"
291
+ )
292
+ page = await context.new_page()
293
+
294
+ try:
295
+ url = "https://www.google.com/search?q=سلام&udm=50"
296
+ await page.goto(url, wait_until="domcontentloaded", timeout=30000)
297
+
298
+ # تزریق ۱۰۰٪ پناهنده فایل به اینپوت‌های مخفی لایه وب گوگل
299
+ if saved_image_path and os.path.exists(saved_image_path):
300
+ try:
301
+ file_input = await page.query_selector("input[type='file']")
302
+ if file_input:
303
+ await file_input.set_input_files(saved_image_path)
304
+ await page.wait_for_timeout(1500)
305
+ except Exception as e:
306
+ print(f"Bypassed image setup: {e}")
307
+
308
+ # درج کوئری متنی کاربر درون کادر چت
309
+ input_box = None
310
+ candidates = await page.query_selector_all("textarea, input, [contenteditable='true']")
311
+ for el in candidates:
312
+ try:
313
+ if await el.is_visible():
314
+ placeholder = await el.get_attribute("placeholder") or ""
315
+ if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]):
316
+ input_box = el
317
+ break
318
+ except:
319
+ continue
320
 
321
+ if not input_box:
322
+ input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']")
323
+
324
+ if input_box:
325
+ await input_box.focus()
326
+ await input_box.fill(message)
327
+ await page.wait_for_timeout(300)
328
+ await input_box.press("Enter")
329
 
330
+ # فایر کردن دکمه فیزیکی ارسال جهت اطمینان فرم در کلاینت‌های کند
331
+ try:
332
+ send_buttons = await page.query_selector_all("button, [role='button']")
333
+ for btn in send_buttons:
334
+ if await btn.is_visible():
335
+ aria = (await btn.get_attribute("aria-label") or "").lower()
336
+ if any(w in aria for w in ["send", "ارسال", "arrow", "up"]):
337
+ await btn.click(timeout=1000)
338
+ break
339
+ except:
340
+ pass
341
+
342
+ # 🚀 حلقه مانیتورینگ استریمینگ: واکشی کلمات در زمان واقعی بدون ثانیه‌ای معطلی ثمر بخش
343
+ last_sent_text = ""
344
+ no_change_count = 0
345
 
346
+ for _ in range(40): # حداکثر ۱۶ ثانیه پایش زنده صفحه
347
+ await page.wait_for_timeout(400) # بررسی صفحه هر ۴۰۰ میلی‌ثانیه برای استریم فوق سریع
348
+ full_text = await page.evaluate("() => document.body.innerText")
349
+ current_clean_text = clean_text(full_text, message)
350
+
351
+ # اگر متن جدیدی تایپ شده بود، تفاوت آن را فوراً روانه کلاینت کن
352
+ if len(current_clean_text) > len(last_sent_text):
353
+ new_chunk = current_clean_text[len(last_sent_text):]
354
+ yield new_chunk
355
+ last_sent_text = current_clean_text
356
+ no_change_count = 0
357
+ else:
358
+ if len(current_clean_text) > 0:
359
+ no_change_count += 1
360
+
361
+ # اگر کادر هشدار گوگل لود شده بود یا تایپ متن به پایان رسیده بود، استریم را ببند
362
+ if "هوشواره ممکن است اشتباه" in full_text or "بیشتر بدانید" in full_text or "پاسخ‌های مربوط به افراد" in full_text:
363
+ # ارسال نهایی باقیمانده کاراکترها برای احتیاط
364
+ full_text = await page.evaluate("() => document.body.innerText")
365
+ current_clean_text = clean_text(full_text, message)
366
+ if len(current_clean_text) > len(last_sent_text):
367
+ yield current_clean_text[len(last_sent_text):]
368
+ break
369
+
370
+ # اگر برای ۳ ثانیه هیچ متنی اضافه نشد یعنی پاسخ متوقف شده است
371
+ if no_change_count > 7 and len(last_sent_text) > 0:
372
+ break
373
+
374
+ except Exception as e:
375
+ yield f"خطای پردازش هوش مصنوعی: {str(e)}"
376
+ finally:
377
+ await context.close()
378
+ if saved_image_path and os.path.exists(saved_image_path):
379
+ try: os.remove(saved_image_path)
380
+ except: pass
381
+
382
+ return StreamingResponse(response_generator(), media_type="text/event-stream")
383
+
384
+ if __name__ == "__main__":
385
+ uvicorn.run(app, host="0.0.0.0", port=7860)