import gradio as gr import pandas as pd from urllib.parse import urlparse import time import requests from io import BytesIO import json # Try to import plotting libraries with fallbacks try: import matplotlib.pyplot as plt MATPLOTLIB_AVAILABLE = True print("✅ Matplotlib successfully imported") except ImportError: print("⚠️ Matplotlib not found, attempting to install...") try: import subprocess import sys subprocess.check_call([sys.executable, "-m", "pip", "install", "matplotlib", "--quiet"]) import matplotlib.pyplot as plt MATPLOTLIB_AVAILABLE = True print("✅ Matplotlib installed and imported successfully") except Exception as e: print(f"❌ Failed to install Matplotlib: {e}") MATPLOTLIB_AVAILABLE = False # ===================== URL Validation ===================== def is_instagram_url(url: str) -> bool: """Validate if URL is a proper Instagram URL""" try: url = url.strip() if not url: return False # Add https if missing if not url.startswith(('http://', 'https://')): url = 'https://' + url parsed = urlparse(url) domain = parsed.netloc.lower() # Check if it's Instagram domain if 'instagram.com' not in domain: return False # Check if it has a valid path (not just homepage) if not parsed.path or parsed.path in ['/', '']: return False return True except Exception as e: print(f"URL validation error: {e}") return False def check_url_accessible(url: str, timeout: int = 5) -> bool: """Lenient check for public reachability of the URL (Instagram often blocks bots).""" try: if not url.startswith(('http://', 'https://')): url = 'https://' + url headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Connection': 'keep-alive', } response = requests.head(url, allow_redirects=True, timeout=timeout, headers=headers) print(f"URL check response: {response.status_code}") return response.status_code in (200, 301, 302, 403, 429) or response.status_code < 500 except requests.exceptions.Timeout: print("URL check timed out - assuming URL is valid") return True except requests.exceptions.ConnectionError: print("Connection error during URL check - assuming URL is valid") return True except Exception as e: print(f"URL accessibility check failed: {e}") return True def test_file_generation(): """Test function to generate sample files with fake comments.""" try: sample_data = pd.DataFrame({ 'Пользователь': ['user1', 'user2', 'user3'], 'Комментарий': ['Тест 1', 'Тест 2', 'Тест 3'], 'Дата': ['2025-09-20', '2025-09-20', '2025-09-20'], 'Тональность': ['позитивный', 'нейтральный', 'негативный'], 'Категория': ['вопрос', 'отзыв', 'жалоба'], 'Модерация': ['безопасно', 'безопасно', 'безопасно'], 'Автоответ': ['Ответ 1', 'Ответ 2', 'Ответ 3'] }) categories = sample_data['Категория'].value_counts() sentiments = sample_data['Тональность'].value_counts() files = create_report_files(sample_data, categories, sentiments) if files: return f"✅ Тестовые файлы созданы: {len(files)} файлов", files else: return "❌ Ошибка создания тестовых файлов", [] except Exception as e: return f"❌ Ошибка тестирования: {str(e)}", [] # ===================== Charts with Matplotlib Only ===================== def make_charts(categories: pd.Series, sentiments: pd.Series): """Create visualization charts with pure Matplotlib""" if not MATPLOTLIB_AVAILABLE: print("No plotting libraries available") return None, None try: # Category pie chart fig_cat, ax_cat = plt.subplots(figsize=(8, 6)) if not categories.empty: # Manual color palette colors = ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5'] colors = colors[:len(categories)] wedges, texts, autotexts = ax_cat.pie( categories.values, labels=categories.index, autopct='%1.1f%%', startangle=140, colors=colors ) ax_cat.set_title("Распределение по категориям", color="#E6007E", fontsize=14, fontweight='bold') # Style the text for autotext in autotexts: autotext.set_color('white') autotext.set_fontweight('bold') else: ax_cat.text(0.5, 0.5, "Нет данных для отображения", ha="center", va="center", fontsize=12) ax_cat.set_xlim(0, 1) ax_cat.set_ylim(0, 1) # Sentiment bar chart fig_sent, ax_sent = plt.subplots(figsize=(8, 6)) if not sentiments.empty: colors = { 'позитивный': '#28a745', 'негативный': '#dc3545', 'нейтральный': '#6c757d', 'positive': '#28a745', 'negative': '#dc3545', 'neutral': '#6c757d' } bar_colors = [colors.get(sent, '#E6007E') for sent in sentiments.index] bars = ax_sent.bar(sentiments.index, sentiments.values, color=bar_colors) ax_sent.set_title("Распределение по тональности", color="#E6007E", fontsize=14, fontweight='bold') ax_sent.set_ylabel("Количество", fontsize=12) ax_sent.set_xlabel("Тональность", fontsize=12) # Add value labels on bars for bar in bars: height = bar.get_height() ax_sent.text(bar.get_x() + bar.get_width()/2., height, f'{int(height)}', ha='center', va='bottom', fontweight='bold') # Rotate x labels if needed plt.setp(ax_sent.get_xticklabels(), rotation=45, ha='right') else: ax_sent.text(0.5, 0.5, "Нет данных для отображения", ha="center", va="center", fontsize=12) ax_sent.set_xlim(0, 1) ax_sent.set_ylim(0, 1) plt.tight_layout() return fig_cat, fig_sent except Exception as e: print(f"Chart creation error: {e}") return None, None # ===================== File Generation ===================== def create_report_files(df: pd.DataFrame, categories: pd.Series, sentiments: pd.Series): """Create CSV and Excel report files (robust & portable)""" import os import tempfile # Ensure non-empty frame for saving if df is None or df.empty: print("DataFrame is empty, creating minimal report") df = pd.DataFrame({"Сообщение": ["Нет данных для отображения"]}) try: # Create files with timestamp for uniqueness timestamp = time.strftime("%Y%m%d_%H%M%S") # Use system temp directory temp_dir = tempfile.gettempdir() csv_path = os.path.join(temp_dir, f"instagram_report_{timestamp}.csv") xlsx_path = os.path.join(temp_dir, f"instagram_report_{timestamp}.xlsx") print(f"Creating files in: {temp_dir}") # Save CSV (UTF-8 with BOM so Excel opens Cyrillic cleanly) df.to_csv(csv_path, index=False, encoding="utf-8-sig") print(f"CSV created: {csv_path} ({os.path.getsize(csv_path)} bytes)") # Excel writer with comprehensive error handling summary_rows = { 'Метрика': [ 'Всего комментариев', 'Уникальных пользователей', 'Уникальных категорий', 'Уникальных тональностей' ], 'Значение': [ int(len(df)) if len(df) > 0 else 0, int(df["Пользователь"].nunique()) if "Пользователь" in df.columns and len(df) > 0 else 0, int(len(categories)) if isinstance(categories, pd.Series) and not categories.empty else 0, int(len(sentiments)) if isinstance(sentiments, pd.Series) and not sentiments.empty else 0, ] } # Try multiple Excel engines excel_created = False # Try openpyxl first try: with pd.ExcelWriter(xlsx_path, engine="openpyxl") as writer: df.to_excel(writer, sheet_name="Комментарии", index=False) pd.DataFrame(summary_rows).to_excel(writer, sheet_name="Сводка", index=False) if isinstance(categories, pd.Series) and not categories.empty: cat_df = categories.reset_index() cat_df.columns = ['Категория', 'Количество'] cat_df.to_excel(writer, sheet_name="Категории", index=False) if isinstance(sentiments, pd.Series) and not sentiments.empty: sen_df = sentiments.reset_index() sen_df.columns = ['Тональность', 'Количество'] sen_df.to_excel(writer, sheet_name="Тональности", index=False) excel_created = True print(f"Excel created with openpyxl: {xlsx_path} ({os.path.getsize(xlsx_path)} bytes)") except Exception as e: print(f"openpyxl failed: {e}") # Try xlsxwriter as fallback try: import xlsxwriter with pd.ExcelWriter(xlsx_path, engine="xlsxwriter") as writer: df.to_excel(writer, sheet_name="Комментарии", index=False) pd.DataFrame(summary_rows).to_excel(writer, sheet_name="Сводка", index=False) excel_created = True print(f"Excel created with xlsxwriter: {xlsx_path}") except Exception as e2: print(f"xlsxwriter also failed: {e2}") # Last resort: basic Excel file try: df.to_excel(xlsx_path, index=False) excel_created = True print(f"Basic Excel created: {xlsx_path}") except Exception as e3: print(f"All Excel methods failed: {e3}") # Verify files exist and are readable files_to_return = [] # Check CSV if os.path.exists(csv_path) and os.path.getsize(csv_path) > 0: files_to_return.append(csv_path) print(f"CSV file ready: {os.path.basename(csv_path)}") else: print("CSV file creation failed") # Check Excel if excel_created and os.path.exists(xlsx_path) and os.path.getsize(xlsx_path) > 0: files_to_return.append(xlsx_path) print(f"Excel file ready: {os.path.basename(xlsx_path)}") else: print("Excel file creation failed") print(f"Total files ready for download: {len(files_to_return)}") return files_to_return except Exception as e: print(f"File creation error: {e}") import traceback traceback.print_exc() return [] # ===================== Main Processing Function ===================== def process_instagram_url(url: str): """Main function to process Instagram URL and return analysis results""" # Initialize empty dataframes for consistent return structure empty_df = pd.DataFrame(columns=["Пользователь", "Комментарий", "Дата", "Тональность", "Категория", "Модерация", "Автоответ"]) empty_moderation = pd.DataFrame(columns=["Пользователь", "Комментарий", "Модерация"]) empty_answers = pd.DataFrame(columns=["Пользователь", "Комментарий", "Автоответ"]) # Validate URL if not url or not url.strip(): return ( "❌ Пожалуйста, введите Instagram URL", empty_df, empty_moderation, empty_answers, "Введите валидную Instagram ссылку для начала анализа.", None, None, [] ) if not is_instagram_url(url): return ( "❌ Это не валидная Instagram ссылка. Пожалуйста, введите корректную ссылку.", empty_df, empty_moderation, empty_answers, "Неверный формат ссылки Instagram.", None, None, [] ) # Check URL accessibility (but don't fail if check fails) print(f"Checking URL accessibility: {url}") url_accessible = check_url_accessible(url) if not url_accessible: print(f"⚠️ URL accessibility check failed, but continuing anyway...") # Send request to webhook try: webhook_url = "https://azamat-m.app.n8n.cloud/webhook/instagram" payload = {"urls": [url.strip()]} headers = { "Content-Type": "application/json", "User-Agent": "InstagramAnalyzer/1.0" } print(f"Sending request to webhook: {payload}") response = requests.post(webhook_url, json=payload, headers=headers, timeout=500) # Increased timeout to 500 seconds print(f"Webhook response status: {response.status_code}") print(f"Response headers: {dict(response.headers)}") # Log first 200 chars of response for debugging if hasattr(response, 'text'): print(f"Response preview: {response.text[:200]}...") except requests.exceptions.Timeout: return ( "❌ Превышено время ожидания ответа от сервера (500 сек). Попробуйте позже.", empty_df, empty_moderation, empty_answers, "Тайм-аут запроса к серверу.", None, None, [] ) except requests.exceptions.ConnectionError: return ( "❌ Ошибка подключения к серверу анализа. Проверьте интернет-соединение.", empty_df, empty_moderation, empty_answers, "Ошибка подключения к серверу.", None, None, [] ) except Exception as e: return ( f"❌ Ошибка при отправке запроса: {str(e)}", empty_df, empty_moderation, empty_answers, f"Ошибка запроса: {str(e)}", None, None, [] ) # Check response status if response.status_code != 200: return ( f"⚠️ Сервер вернул код ошибки {response.status_code}. Попробуйте позже.", empty_df, empty_moderation, empty_answers, f"Ошибка сервера: HTTP {response.status_code}", None, None, [] ) # Parse response try: data = response.json() print(f"Received data type: {type(data)}, length: {len(data) if isinstance(data, list) else 'N/A'}") except json.JSONDecodeError as e: return ( "❌ Сервер вернул некорректный ответ. Попробуйте позже.", empty_df, empty_moderation, empty_answers, f"Ошибка парсинга ответа: {str(e)}", None, None, [] ) # Validate data format if not isinstance(data, list) or len(data) == 0: return ( "✅ Запрос выполнен успешно, но комментарии не найдены.", empty_df, empty_moderation, empty_answers, "Комментарии не найдены. Возможно, пост не содержит комментариев или они скрыты.", None, None, [] ) # Process data try: processed_rows = [] for item in data: # Extract all available fields user = item.get("user", "") comment = item.get("comment", item.get("chatInput", "")) created_at = item.get("created_at", "") sentiment = item.get("sentiment", "neutral") category = item.get("category", "общее") harmful = item.get("harmful_content", "none") auto_answer = item.get("output", "") # Format creation date if available formatted_date = "" if created_at: try: from datetime import datetime dt = datetime.fromisoformat(created_at.replace('Z', '+00:00')) formatted_date = dt.strftime("%Y-%m-%d %H:%M") except: formatted_date = created_at # Translate sentiment values to Russian if needed sentiment_mapping = { "positive": "позитивный", "negative": "негативный", "neutral": "нейтральный" } sentiment_ru = sentiment_mapping.get(sentiment.lower(), sentiment) # Translate category values to Russian if needed category_mapping = { "question": "вопрос", "complaint": "жалоба", "review": "отзыв", "general": "общее" } category_ru = category_mapping.get(category.lower(), category) # Translate harmful_content values moderation_mapping = { "none": "безопасно", "toxic": "токсичный", "spam": "спам" } moderation_ru = moderation_mapping.get(harmful.lower(), harmful) processed_rows.append({ "Пользователь": user, "Комментарий": comment, "Дата": formatted_date, "Тональность": sentiment_ru, "Категория": category_ru, "Модерация": moderation_ru, "Автоответ": auto_answer }) # Create dataframes df_all = pd.DataFrame(processed_rows) df_moderation = df_all[["Пользователь", "Комментарий", "Модерация"]].copy() df_answers = df_all[["Пользователь", "Комментарий", "Автоответ"]].copy() # Calculate statistics total_comments = len(df_all) unique_users = df_all["Пользователь"].nunique() if "Пользователь" in df_all.columns else 0 categories = df_all["Категория"].value_counts() sentiments = df_all["Тональность"].value_counts() # Create statistics markdown stats_text = f""" **📊 Общая статистика:** - **Всего комментариев:** {total_comments} - **Уникальных пользователей:** {unique_users} **📂 По категориям:** {chr(10).join([f'- **{category}:** {count}' for category, count in categories.items()])} **💭 По тональности:** {chr(10).join([f'- **{sentiment}:** {count}' for sentiment, count in sentiments.items()])} """.strip() # Create charts fig_categories, fig_sentiments = make_charts(categories, sentiments) # Create report files print("Creating report files...") report_files = create_report_files(df_all, categories, sentiments) print(f"Report files created: {report_files}") success_message = f"✅ Успешно обработано {total_comments} комментариев от {unique_users} пользователей!" return ( success_message, df_all, df_moderation, df_answers, stats_text, fig_categories, fig_sentiments, report_files ) except Exception as e: print(f"Data processing error: {e}") return ( f"❌ Ошибка при обработке данных: {str(e)}", empty_df, empty_moderation, empty_answers, f"Ошибка обработки: {str(e)}", None, None, [] ) # ===================== Custom CSS ===================== custom_css = """ /* Altel brand colors and styling */ .gradio-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1200px; margin: 0 auto; } /* Headers and titles */ .gradio-container h1, .gradio-container h2, .gradio-container h3 { color: #E6007E !important; font-weight: 600; } /* Primary buttons */ .gradio-container .primary { background: linear-gradient(135deg, #E6007E 0%, #C5006C 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; box-shadow: 0 2px 4px rgba(230, 0, 126, 0.3) !important; transition: all 0.3s ease !important; } .gradio-container .primary:hover { transform: translateY(-1px) !important; box-shadow: 0 4px 8px rgba(230, 0, 126, 0.4) !important; } /* Tab styling */ .gradio-container .tab-nav button { color: #E6007E !important; border-bottom: 2px solid transparent !important; } .gradio-container .tab-nav button.selected { color: #E6007E !important; border-bottom: 2px solid #E6007E !important; font-weight: 600 !important; } /* Table headers */ .gradio-container table thead th { background-color: #E6007E !important; color: white !important; font-weight: 600 !important; } /* Cards and blocks */ .gradio-container .block { border-radius: 12px !important; border: 1px solid #f0f0f0 !important; box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; } /* Status messages */ .gradio-container .textbox textarea[readonly] { background-color: #f8f9fa !important; border-left: 4px solid #E6007E !important; } """ # ===================== Gradio Interface ===================== def create_app(): """Create the Gradio application""" with gr.Blocks(css=custom_css, title="Instagram Comment Analyzer", theme=gr.themes.Soft()) as app: # Header gr.Markdown(""" # 📸 AltelPikir Анализ комментариев Instagram с помощью ИИ. Получите детальную аналитику тональности, категоризацию и модерацию контента. **Как использовать:** 1. Вставьте ссылку на публичный пост Instagram 2. Нажмите "Анализировать комментарии" 3. Просмотрите результаты в различных вкладках """) # Input section with gr.Row(): with gr.Column(scale=4): url_input = gr.Textbox( label="🔗 Instagram URL", placeholder="https://www.instagram.com/p/XXXXXXXXX/", info="Введите ссылку на пост, рилс или IGTV" ) with gr.Column(scale=1): analyze_btn = gr.Button( "🚀 Анализировать комментарии", variant="primary", size="lg" ) # Status output status_output = gr.Textbox( label="📋 Статус обработки", interactive=False, lines=2 ) # Results tabs with gr.Tabs(): with gr.Tab("💬 Все комментарии"): comments_df = gr.Dataframe( headers=["Пользователь", "Комментарий", "Дата", "Тональность", "Категория", "Модерация", "Автоответ"], interactive=False, wrap=True ) with gr.Tab("🛡️ Модерация"): moderation_df = gr.Dataframe( headers=["Пользователь", "Комментарий", "Модерация"], interactive=False, wrap=True ) with gr.Tab("🤖 Автоответы"): answers_df = gr.Dataframe( headers=["Пользователь", "Комментарий", "Автоответ"], interactive=False, wrap=True ) with gr.Tab("📊 Аналитика"): with gr.Row(): stats_markdown = gr.Markdown("Загрузите Instagram ссылку для просмотра статистики.") # Show charts if matplotlib is available if MATPLOTLIB_AVAILABLE: with gr.Row(): with gr.Column(): categories_chart = gr.Plot(label="Распределение по категориям") with gr.Column(): sentiments_chart = gr.Plot(label="Распределение по тональности") else: gr.Markdown("⚠️ **Графики недоступны:** Matplotlib не установлен. Статистика доступна в текстовом формате выше.") categories_chart = gr.HTML(visible=False) sentiments_chart = gr.HTML(visible=False) download_files = gr.File( label="📁 Скачать отчеты (CSV + Excel)", file_count="multiple", file_types=[".csv", ".xlsx"], interactive=False, visible=True ) # Add test file generation button with gr.Row(): test_files_btn = gr.Button("🧪 Создать тестовые файлы", variant="secondary") test_status = gr.Textbox(label="Статус теста", interactive=False, visible=False) test_files_output = gr.File(label="Тестовые файлы", file_count="multiple", visible=False) # Connect the processing function analyze_btn.click( fn=process_instagram_url, inputs=[url_input], outputs=[ status_output, comments_df, moderation_df, answers_df, stats_markdown, categories_chart, sentiments_chart, download_files ] ) # Connect test function test_files_btn.click( fn=test_file_generation, inputs=[], outputs=[test_status, test_files_output] ).then( lambda: (gr.update(visible=True), gr.update(visible=True)), outputs=[test_status, test_files_output] ) return app # ===================== Launch Application ===================== if __name__ == "__main__": app = create_app() app.launch( share=False, server_name="0.0.0.0", server_port=7860 )