| import os |
| import time |
| import asyncio |
| import uvicorn |
| from fastapi import FastAPI, Form, File, UploadFile, HTTPException |
| from fastapi.responses import HTMLResponse, JSONResponse |
| from playwright.async_api import async_playwright |
|
|
| app = FastAPI() |
| UPLOAD_DIR = "temp_uploads" |
| os.makedirs(UPLOAD_DIR, exist_ok=True) |
|
|
| |
| HTML_CHAT_INTERFACE = """ |
| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Google AI Free Chatbot + Vision</title> |
| <style> |
| :root { |
| --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); |
| --glass-bg: rgba(30, 41, 59, 0.7); |
| --glass-border: rgba(255, 255, 255, 0.08); |
| --text-main: #f8fafc; |
| --accent-color: #38bdf8; |
| --user-bubble: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); |
| --ai-bubble: rgba(255, 255, 255, 0.06); |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Tahoma, sans-serif; |
| margin: 0; |
| padding: 0; |
| background: var(--bg-gradient); |
| color: var(--text-main); |
| height: 100vh; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| overflow: hidden; |
| } |
| |
| .chat-container { |
| width: 100%; |
| max-width: 850px; |
| height: 85vh; |
| background: var(--glass-bg); |
| backdrop-filter: blur(16px); |
| -webkit-backdrop-filter: blur(16px); |
| border: 1px solid var(--glass-border); |
| border-radius: 24px; |
| display: flex; |
| flex-direction: column; |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); |
| margin: 20px; |
| } |
| |
| .chat-header { |
| padding: 20px 24px; |
| border-bottom: 1px solid var(--glass-border); |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| |
| .chat-header .status-dot { |
| width: 10px; |
| height: 10px; |
| background: #4ade80; |
| border-radius: 50%; |
| box-shadow: 0 0 10px #4ade80; |
| } |
| |
| .chat-header h2 { |
| margin: 0; |
| font-size: 18px; |
| font-weight: 600; |
| color: var(--accent-color); |
| } |
| |
| .messages-box { |
| flex: 1; |
| padding: 24px; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| |
| .messages-box::-webkit-scrollbar { width: 6px; } |
| .messages-box::-webkit-scrollbar-track { background: transparent; } |
| .messages-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 10px; } |
| |
| .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; |
| } |
| |
| .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); |
| } |
| |
| .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; |
| } |
| |
| .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; |
| } |
| |
| .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); |
| } |
| |
| .preview-container img { |
| width: 40px; |
| height: 40px; |
| object-fit: cover; |
| border-radius: 6px; |
| } |
| |
| .preview-container .remove-btn { |
| color: #ef4444; |
| cursor: pointer; |
| font-weight: bold; |
| font-size: 14px; |
| } |
| |
| .input-area { |
| display: flex; |
| gap: 12px; |
| align-items: center; |
| } |
| |
| .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; |
| } |
| |
| .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); |
| } |
| |
| .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; |
| } |
| |
| .upload-label:hover { |
| background: rgba(255, 255, 255, 0.1); |
| border-color: var(--accent-color); |
| } |
| |
| .upload-label svg { |
| width: 22px; |
| height: 22px; |
| fill: var(--text-main); |
| } |
| |
| .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; |
| } |
| |
| .input-area button:hover { |
| background: #7dd3fc; |
| transform: translateY(-1px); |
| } |
| |
| .input-area button:disabled { |
| background: #64748b; |
| color: #1e293b; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .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); |
| } |
| |
| .typing-indicator span { |
| width: 8px; |
| height: 8px; |
| background: var(--accent-color); |
| border-radius: 50%; |
| animation: bounce 1.4s infinite ease-in-out both; |
| } |
| .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } |
| .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } |
| |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } } |
| </style> |
| </head> |
| <body> |
| |
| <div class="chat-container"> |
| <div class="chat-header"> |
| <div class="status-dot"></div> |
| <h2>دستیار مالتیمدال گوگل (متن + تصویر کاملاً رایگان)</h2> |
| </div> |
| <div class="messages-box" id="messagesBox"> |
| <div class="message ai">سلام! من متصل به هوشواره گوگل هستم. میتوانید علاوه بر متن، تصویر هم برایم بفرستید تا بررسی کنم.</div> |
| </div> |
| |
| <div class="input-wrapper"> |
| <div class="preview-container" id="previewContainer"> |
| <img src="" id="imagePreview"> |
| <span class="remove-btn" onclick="removeSelectedImage()">✕ حذف</span> |
| </div> |
| |
| <div class="input-area"> |
| <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="previewImage(event)"> |
| <label for="fileInput" class="upload-label" title="آپلود تصویر"> |
| <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> |
| </label> |
| |
| <input type="text" id="userInput" placeholder="پیام یا توضیح مربوط به تصویر را بنویسید..." onkeypress="handleKeyPress(event)"> |
| <button id="sendBtn" onclick="sendMessage()">ارسال</button> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| const messagesBox = document.getElementById('messagesBox'); |
| const userInput = document.getElementById('userInput'); |
| const fileInput = document.getElementById('fileInput'); |
| const sendBtn = document.getElementById('sendBtn'); |
| const previewContainer = document.getElementById('previewContainer'); |
| const imagePreview = document.getElementById('imagePreview'); |
| |
| function handleKeyPress(e) { |
| if (e.key === 'Enter') sendMessage(); |
| } |
| |
| function previewImage(event) { |
| const file = event.target.files[0]; |
| if (file) { |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| imagePreview.src = e.target.result; |
| previewContainer.style.display = 'flex'; |
| } |
| reader.readAsDataURL(file); |
| } |
| } |
| |
| function removeSelectedImage() { |
| fileInput.value = ''; |
| previewContainer.style.display = 'none'; |
| imagePreview.src = ''; |
| } |
| |
| async function sendMessage() { |
| const text = userInput.value.trim(); |
| const file = fileInput.files[0]; |
| if (!text && !file) return; |
| |
| if (file) appendImageMessage(imagePreview.src, 'user'); |
| if (text) appendMessage(text, 'user'); |
| |
| userInput.value = ''; |
| removeSelectedImage(); |
| setLoading(true); |
| |
| const formData = new FormData(); |
| formData.append('message', text || "این تصویر را تحلیل کن"); |
| if (file) formData.append('image', file); |
| |
| try { |
| const response = await fetch('/api/chat', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| |
| if (response.ok) { |
| appendMessage(data.response, 'ai'); |
| } else { |
| appendMessage("خطا: " + (data.detail || "مشکلی در دریافت پاسخ رخ داد."), 'ai'); |
| } |
| } catch (error) { |
| appendMessage("خطای شبکه: دریافت پاسخ از سرور انجام نشد.", 'ai'); |
| } finally { |
| setLoading(false); |
| } |
| } |
| |
| function appendMessage(text, sender) { |
| const msgDiv = document.createElement('div'); |
| msgDiv.classList.add('message', sender); |
| msgDiv.innerText = text; |
| messagesBox.appendChild(msgDiv); |
| messagesBox.scrollTop = messagesBox.scrollHeight; |
| } |
| |
| function appendImageMessage(src, sender) { |
| const msgDiv = document.createElement('div'); |
| msgDiv.classList.add('message', sender); |
| msgDiv.style.padding = '8px'; |
| const img = document.createElement('img'); |
| img.src = src; |
| img.style.maxWidth = '200px'; |
| img.style.maxHeight = '200px'; |
| img.style.borderRadius = '12px'; |
| img.style.display = 'block'; |
| msgDiv.appendChild(img); |
| messagesBox.appendChild(msgDiv); |
| messagesBox.scrollTop = messagesBox.scrollHeight; |
| } |
| |
| function setLoading(isLoading) { |
| if (isLoading) { |
| userInput.disabled = true; |
| sendBtn.disabled = true; |
| const indicator = document.createElement('div'); |
| indicator.classList.add('typing-indicator'); |
| indicator.id = 'typingIndicator'; |
| indicator.innerHTML = '<span></span><span></span><span></span>'; |
| messagesBox.appendChild(indicator); |
| messagesBox.scrollTop = messagesBox.scrollHeight; |
| } else { |
| userInput.disabled = false; |
| sendBtn.disabled = false; |
| const indicator = document.getElementById('typingIndicator'); |
| if (indicator) indicator.remove(); |
| userInput.focus(); |
| } |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def chat_interface(): |
| return HTML_CHAT_INTERFACE |
|
|
| @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)}") |
|
|
| url = "https://www.google.com/search?q=سلام&udm=50" |
| ai_response = "" |
| |
| async with async_playwright() as p: |
| try: |
| 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="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}, |
| locale="fa-IR", |
| timezone_id="Asia/Tehran" |
| ) |
| |
| page = await context.new_page() |
| await page.goto(url, wait_until="networkidle", timeout=45000) |
| await page.wait_for_timeout(3000) |
| |
| |
| if saved_image_path and os.path.exists(saved_image_path): |
| uploaded = False |
| |
| |
| try: |
| file_input = await page.query_selector("input[type='file']") |
| if file_input: |
| await file_input.set_input_files(saved_image_path) |
| await page.wait_for_timeout(3000) |
| uploaded = True |
| print("Direct file input injection succeeded.") |
| except Exception as e: |
| print(f"Direct injection bypassed: {e}") |
|
|
| |
| if not uploaded: |
| try: |
| |
| chat_inputs = await page.query_selector_all("textarea, input[type='text'], [contenteditable='true']") |
| for inp in chat_inputs: |
| try: |
| if await inp.is_visible(): |
| await inp.click(timeout=2000) |
| await page.wait_for_timeout(500) |
| break |
| except: |
| continue |
| |
| |
| elements = await page.query_selector_all("button, div[role='button'], svg, [aria-label]") |
| for el in elements: |
| try: |
| if await el.is_visible(): |
| aria = (await el.get_attribute("aria-label") or "").lower() |
| html = (await el.inner_html() or "").lower() |
| text = (await el.inner_text() or "").lower() |
| combined = aria + html + text |
| |
| if any(k in combined for k in ["+", "plus", "camera", "lens", "دوربین", "تصویر", "عکس"]): |
| async with page.expect_file_chooser(timeout=3000) as fc_info: |
| await el.click(timeout=2000) |
| file_chooser = await fc_info.value |
| await file_chooser.set_files(saved_image_path) |
| await page.wait_for_timeout(3000) |
| uploaded = True |
| print("Uploaded via main UI icon trigger.") |
| break |
| except: |
| continue |
| except Exception as e: |
| print(f"UI icon flow failed: {e}") |
|
|
| |
| if not uploaded: |
| try: |
| menu_items = await page.query_selector_all("div, span, button, p") |
| for item in menu_items: |
| try: |
| item_text = (await item.inner_text() or "").strip() |
| if item_text in ["گالری", "فایلها", "Gallery", "Files"]: |
| if await item.is_visible(): |
| async with page.expect_file_chooser(timeout=3000) as fc_info: |
| await item.click(timeout=2000) |
| file_chooser = await fc_info.value |
| await file_chooser.set_files(saved_image_path) |
| await page.wait_for_timeout(3000) |
| uploaded = True |
| print("Uploaded via bottom sheet item click.") |
| break |
| except: |
| continue |
| except Exception as e: |
| print(f"Bottom sheet workflow failed: {e}") |
|
|
| |
| input_box = None |
| candidates = await page.query_selector_all("textarea, input, [contenteditable='true']") |
| for el in candidates: |
| try: |
| if await el.is_visible(): |
| placeholder = await el.get_attribute("placeholder") or "" |
| if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]): |
| input_box = el |
| break |
| except: |
| continue |
| |
| if not input_box: |
| input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']") |
|
|
| if input_box: |
| await input_box.focus() |
| await input_box.fill(message) |
| await page.wait_for_timeout(500) |
| await input_box.press("Enter") |
| |
| |
| try: |
| send_buttons = await page.query_selector_all("button, [role='button']") |
| for btn in send_buttons: |
| if await btn.is_visible(): |
| aria = (await btn.get_attribute("aria-label") or "").lower() |
| if any(w in aria for w in ["send", "ارسال", "arrow", "up"]): |
| await btn.click(timeout=2000) |
| break |
| except: |
| pass |
|
|
| |
| await page.wait_for_timeout(9000) |
| |
| full_text = await page.evaluate("() => document.body.innerText") |
| await browser.close() |
| |
| |
| lines = full_text.split('\n') |
| cleaned_lines = [] |
| skip_headers = ["تصاویر", "ویدیوها", "اخبار", "نقشهها", "خرید کردن", "کتابها", "مالی", "حالت موضوعمحور", "ورود"] |
| |
| for line in lines: |
| line_str = line.strip() |
| if not line_str: |
| continue |
| if any(header in line_str for header in skip_headers): |
| continue |
| if "در پاسخهای" in line_str or "بیشتر بدانید" in line_str: |
| continue |
| cleaned_lines.append(line_str) |
| |
| ai_response = "\n".join(cleaned_lines) |
| if not ai_response: |
| ai_response = full_text |
| |
| except Exception as e: |
| if 'browser' in locals(): |
| await browser.close() |
| raise HTTPException(status_code=500, detail=f"خطا در رندر سرور هوش مصنوعی: {str(e)}") |
| finally: |
| |
| if saved_image_path and os.path.exists(saved_image_path): |
| try: |
| os.remove(saved_image_path) |
| except: |
| pass |
| |
| return JSONResponse(content={"response": ai_response}) |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|