futureDoctor commited on
Commit
c047192
·
verified ·
1 Parent(s): 12452c1
Files changed (1) hide show
  1. app.py +646 -0
app.py ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import gradio as gr
4
+ import pandas as pd
5
+ from urllib.parse import urlparse
6
+ import time
7
+ import requests
8
+ from io import BytesIO
9
+ import json
10
+ import seaborn as sns
11
+ import matplotlib.pyplot as plt
12
+
13
+ # Try to import Plotly, install if missing, fallback gracefully
14
+ try:
15
+ import plotly.express as px
16
+ import plotly.graph_objects as go
17
+ PLOTLY_AVAILABLE = True
18
+ print("✅ Plotly successfully imported")
19
+ except ImportError:
20
+ print("⚠️ Plotly not found, attempting to install...")
21
+ try:
22
+ import subprocess
23
+ import sys
24
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "plotly", "--quiet"])
25
+ import plotly.express as px
26
+ import plotly.graph_objects as go
27
+ PLOTLY_AVAILABLE = True
28
+ print("✅ Plotly installed and imported successfully")
29
+ except Exception as e:
30
+ print(f"❌ Failed to install Plotly: {e}")
31
+ print("📊 Charts will be disabled, but app will work normally")
32
+ PLOTLY_AVAILABLE = False
33
+
34
+ # ===================== URL Validation =====================
35
+ def is_instagram_url(url: str) -> bool:
36
+ """Validate if URL is a proper Instagram URL"""
37
+ try:
38
+ url = url.strip()
39
+ if not url:
40
+ return False
41
+
42
+ # Add https if missing
43
+ if not url.startswith(('http://', 'https://')):
44
+ url = 'https://' + url
45
+
46
+ parsed = urlparse(url)
47
+ domain = parsed.netloc.lower()
48
+
49
+ # Check if it's Instagram domain
50
+ if 'instagram.com' not in domain:
51
+ return False
52
+
53
+ # Check if it has a valid path (not just homepage)
54
+ if not parsed.path or parsed.path in ['/', '']:
55
+ return False
56
+
57
+ return True
58
+ except Exception as e:
59
+ print(f"URL validation error: {e}")
60
+ return False
61
+
62
+ def check_url_accessible(url: str, timeout: int = 5) -> bool:
63
+ """Lenient check for public reachability of the URL (Instagram often blocks bots)."""
64
+ try:
65
+ if not url.startswith(('http://', 'https://')):
66
+ url = 'https://' + url
67
+
68
+ headers = {
69
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
70
+ '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
71
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
72
+ 'Accept-Language': 'en-US,en;q=0.5',
73
+ 'Connection': 'keep-alive',
74
+ }
75
+
76
+ response = requests.head(url, allow_redirects=True, timeout=timeout, headers=headers)
77
+ print(f"URL check response: {response.status_code}")
78
+
79
+ return response.status_code in (200, 301, 302, 403, 429) or response.status_code < 500
80
+
81
+ except requests.exceptions.Timeout:
82
+ print("URL check timed out - assuming URL is valid")
83
+ return True
84
+ except requests.exceptions.ConnectionError:
85
+ print("Connection error during URL check - assuming URL is valid")
86
+ return True
87
+ except Exception as e:
88
+ print(f"URL accessibility check failed: {e}")
89
+ return True
90
+
91
+ def test_file_generation():
92
+ """Test function to generate sample files with fake comments."""
93
+ try:
94
+ sample_data = pd.DataFrame({
95
+ 'Пользователь': ['user1', 'user2', 'user3'],
96
+ 'Комментарий': ['Тест 1', 'Тест 2', 'Тест 3'],
97
+ 'Дата': ['2025-09-20', '2025-09-20', '2025-09-20'],
98
+ 'Тональность': ['позитивный', 'нейтральный', 'негативный'],
99
+ 'Категория': ['вопрос', 'отзыв', 'жалоба'],
100
+ 'Модерация': ['безопасно', 'безопасно', 'безопасно'],
101
+ 'Автоответ': ['Ответ 1', 'Ответ 2', 'Ответ 3']
102
+ })
103
+
104
+ categories = sample_data['Категория'].value_counts()
105
+ sentiments = sample_data['Тональность'].value_counts()
106
+
107
+ files = create_report_files(sample_data, categories, sentiments)
108
+
109
+ if files:
110
+ return f"✅ Тестовые файлы созданы: {len(files)} файлов", files
111
+ else:
112
+ return "❌ Ошибка создания тестовых файлов", []
113
+ except Exception as e:
114
+ return f"❌ Ошибка тестирования: {str(e)}", []
115
+
116
+ # ===================== Charts with Plotly =====================
117
+ def make_charts(categories: pd.Series, sentiments: pd.Series):
118
+ """Create visualization charts with Seaborn"""
119
+ try:
120
+ # Category pie chart (Matplotlib only, since Seaborn doesn’t have native pie)
121
+ fig_cat, ax_cat = plt.subplots()
122
+ if not categories.empty:
123
+ ax_cat.pie(
124
+ categories.values,
125
+ labels=categories.index,
126
+ autopct='%1.1f%%',
127
+ startangle=140,
128
+ colors=sns.color_palette("Set3", len(categories))
129
+ )
130
+ ax_cat.set_title("Распределение по категориям", color="#E6007E")
131
+ else:
132
+ ax_cat.text(0.5, 0.5, "Нет данных для отображения",
133
+ ha="center", va="center")
134
+
135
+ # Sentiment bar chart
136
+ fig_sent, ax_sent = plt.subplots()
137
+ if not sentiments.empty:
138
+ sns.barplot(
139
+ x=sentiments.index,
140
+ y=sentiments.values,
141
+ palette={
142
+ 'позитивный': '#28a745',
143
+ 'негативный': '#dc3545',
144
+ 'нейтральный': '#6c757d',
145
+ 'positive': '#28a745',
146
+ 'negative': '#dc3545',
147
+ 'neutral': '#6c757d'
148
+ },
149
+ ax=ax_sent
150
+ )
151
+ ax_sent.set_title("Распределение по тональности", color="#E6007E")
152
+ ax_sent.set_ylabel("Количество")
153
+ ax_sent.set_xlabel("Тональность")
154
+ else:
155
+ ax_sent.text(0.5, 0.5, "Нет данных для отображения",
156
+ ha="center", va="center")
157
+
158
+ return fig_cat, fig_sent
159
+ except Exception as e:
160
+ print(f"Chart creation error: {e}")
161
+ return None, None
162
+
163
+ # ===================== File Generation =====================
164
+ def create_report_files(df: pd.DataFrame, categories: pd.Series, sentiments: pd.Series):
165
+ """Create CSV and Excel report files (robust & portable)"""
166
+ import os
167
+ import tempfile
168
+
169
+ # Ensure non-empty frame for saving
170
+ if df is None or df.empty:
171
+ df = pd.DataFrame({"Сообщение": ["Нет данных для отображения"]})
172
+
173
+ # Create stable temp files that persist after write
174
+ csv_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
175
+ csv_tmp.close()
176
+ xlsx_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
177
+ xlsx_tmp.close()
178
+
179
+ csv_path = csv_tmp.name
180
+ xlsx_path = xlsx_tmp.name
181
+
182
+ # Save CSV (UTF-8 with BOM so Excel opens Cyrillic cleanly)
183
+ df.to_csv(csv_path, index=False, encoding="utf-8-sig")
184
+
185
+ # Excel writer: try openpyxl, fall back to xlsxwriter, fall back to very simple save
186
+ summary_rows = {
187
+ 'Метрика': [
188
+ 'Всего комментариев',
189
+ 'Уникальных пользователей',
190
+ 'Уникальных категорий',
191
+ 'Уникальных тональностей'
192
+ ],
193
+ 'Значение': [
194
+ int(len(df)),
195
+ int(df["Пользователь"].nunique()) if "Пользователь" in df.columns else 0,
196
+ int(len(categories)) if isinstance(categories, pd.Series) and not categories.empty else 0,
197
+ int(len(sentiments)) if isinstance(sentiments, pd.Series) and not sentiments.empty else 0,
198
+ ]
199
+ }
200
+ try:
201
+ try:
202
+ with pd.ExcelWriter(xlsx_path, engine="openpyxl") as writer:
203
+ df.to_excel(writer, sheet_name="Комментарии", index=False)
204
+ pd.DataFrame(summary_rows).to_excel(writer, sheet_name="Сводка", index=False)
205
+
206
+ if isinstance(categories, pd.Series) and not categories.empty:
207
+ cat_df = categories.reset_index()
208
+ cat_df.columns = ['Категория', 'Количество']
209
+ cat_df.to_excel(writer, sheet_name="Категории", index=False)
210
+
211
+ if isinstance(sentiments, pd.Series) and not sentiments.empty:
212
+ sen_df = sentiments.reset_index()
213
+ sen_df.columns = ['Тональность', 'Количество']
214
+ sen_df.to_excel(writer, sheet_name="Тональности", index=False)
215
+ except Exception:
216
+ with pd.ExcelWriter(xlsx_path, engine="xlsxwriter") as writer:
217
+ df.to_excel(writer, sheet_name="Комментарии", index=False)
218
+ pd.DataFrame(summary_rows).to_excel(writer, sheet_name="Сводка", index=False)
219
+ except Exception:
220
+ # As a very last resort, at least save the main sheet
221
+ df.to_excel(xlsx_path, index=False)
222
+
223
+ files_to_return = [p for p in (csv_path, xlsx_path) if os.path.exists(p) and os.path.getsize(p) > 0]
224
+ return files_to_return
225
+
226
+ # ===================== Main Processing Function =====================
227
+ def process_instagram_url(url: str):
228
+ """Main function to process Instagram URL and return analysis results"""
229
+
230
+ # Initialize empty dataframes for consistent return structure
231
+ empty_df = pd.DataFrame(columns=["Пользователь", "Комментарий", "Дата", "Тональность", "Категория", "Модерация", "Автоответ"])
232
+ empty_moderation = pd.DataFrame(columns=["Пользователь", "Комментарий", "Мод��рация"])
233
+ empty_answers = pd.DataFrame(columns=["Пользователь", "Комментарий", "Автоответ"])
234
+
235
+ # Validate URL
236
+ if not url or not url.strip():
237
+ return (
238
+ "❌ Пожалуйста, введите Instagram URL",
239
+ empty_df, empty_moderation, empty_answers,
240
+ "Введите валидную Instagram ссылку для начала анализа.",
241
+ None, None, []
242
+ )
243
+
244
+ if not is_instagram_url(url):
245
+ return (
246
+ "❌ Это не валидная Instagram ссылка. Пожалуйста, введите корректную ссылку.",
247
+ empty_df, empty_moderation, empty_answers,
248
+ "Неверный формат ссылки Instagram.",
249
+ None, None, []
250
+ )
251
+
252
+ # Check URL accessibility (but don't fail if check fails)
253
+ print(f"Checking URL accessibility: {url}")
254
+ url_accessible = check_url_accessible(url)
255
+ if not url_accessible:
256
+ print(f"⚠️ URL accessibility check failed, but continuing anyway...")
257
+ # Don't return error here - Instagram often blocks automated checks
258
+ # but the API might still work
259
+
260
+ # Send request to webhook
261
+ try:
262
+ webhook_url = "https://azamat-m.app.n8n.cloud/webhook/instagram"
263
+ payload = {"urls": [url.strip()]}
264
+ headers = {
265
+ "Content-Type": "application/json",
266
+ "User-Agent": "InstagramAnalyzer/1.0"
267
+ }
268
+
269
+ print(f"Sending request to webhook: {payload}")
270
+ response = requests.post(webhook_url, json=payload, headers=headers, timeout=200) # Increased timeout to 200 seconds
271
+ print(f"Webhook response status: {response.status_code}")
272
+ print(f"Response headers: {dict(response.headers)}")
273
+
274
+ # Log first 200 chars of response for debugging
275
+ if hasattr(response, 'text'):
276
+ print(f"Response preview: {response.text[:200]}...")
277
+
278
+ except requests.exceptions.Timeout:
279
+ return (
280
+ "❌ Превышено время ожидания ответа от сервера (200 сек). Попробуйте позже.",
281
+ empty_df, empty_moderation, empty_answers,
282
+ "Тайм-аут запроса к серверу.",
283
+ None, None, []
284
+ )
285
+ except requests.exceptions.ConnectionError:
286
+ return (
287
+ "❌ Ошибка подключения к серверу анализа. Проверьте интернет-соединение.",
288
+ empty_df, empty_moderation, empty_answers,
289
+ "Ошибка подключения к серверу.",
290
+ None, None, []
291
+ )
292
+ except Exception as e:
293
+ return (
294
+ f"❌ Ошибка при отправке запроса: {str(e)}",
295
+ empty_df, empty_moderation, empty_answers,
296
+ f"Ошибка запроса: {str(e)}",
297
+ None, None, []
298
+ )
299
+
300
+ # Check response status
301
+ if response.status_code != 200:
302
+ return (
303
+ f"⚠️ Сервер вернул код ошибки {response.status_code}. Попробуйте позже.",
304
+ empty_df, empty_moderation, empty_answers,
305
+ f"Ошибка сервера: HTTP {response.status_code}",
306
+ None, None, []
307
+ )
308
+
309
+ # Parse response
310
+ try:
311
+ data = response.json()
312
+ print(f"Received data type: {type(data)}, length: {len(data) if isinstance(data, list) else 'N/A'}")
313
+ except json.JSONDecodeError as e:
314
+ return (
315
+ "❌ Сервер вернул некорректный ответ. Попробуйте позже.",
316
+ empty_df, empty_moderation, empty_answers,
317
+ f"Ошибка парсинга ответа: {str(e)}",
318
+ None, None, []
319
+ )
320
+
321
+ # Validate data format
322
+ if not isinstance(data, list) or len(data) == 0:
323
+ return (
324
+ "✅ Запрос выполнен успешно, но комментарии не найдены.",
325
+ empty_df, empty_moderation, empty_answers,
326
+ "Комментарии не найдены. Возможно, пост не содержит комментариев или они скрыты.",
327
+ None, None, []
328
+ )
329
+
330
+ # Process data
331
+ try:
332
+ processed_rows = []
333
+ for item in data:
334
+ # Extract all available fields
335
+ user = item.get("user", "")
336
+ comment = item.get("comment", item.get("chatInput", ""))
337
+ created_at = item.get("created_at", "")
338
+ sentiment = item.get("sentiment", "neutral")
339
+ category = item.get("category", "общее")
340
+ harmful = item.get("harmful_content", "none")
341
+ auto_answer = item.get("output", "")
342
+
343
+ # Format creation date if available
344
+ formatted_date = ""
345
+ if created_at:
346
+ try:
347
+ from datetime import datetime
348
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
349
+ formatted_date = dt.strftime("%Y-%m-%d %H:%M")
350
+ except:
351
+ formatted_date = created_at
352
+
353
+ # Translate sentiment values to Russian if needed
354
+ sentiment_mapping = {
355
+ "positive": "позитивный",
356
+ "negative": "негативный",
357
+ "neutral": "нейтральный"
358
+ }
359
+ sentiment_ru = sentiment_mapping.get(sentiment.lower(), sentiment)
360
+
361
+ # Translate category values to Russian if needed
362
+ category_mapping = {
363
+ "question": "вопрос",
364
+ "complaint": "жалоба",
365
+ "review": "отзыв",
366
+ "general": "общее"
367
+ }
368
+ category_ru = category_mapping.get(category.lower(), category)
369
+
370
+ # Translate harmful_content values
371
+ moderation_mapping = {
372
+ "none": "безопасно",
373
+ "toxic": "токсичный",
374
+ "spam": "спам"
375
+ }
376
+ moderation_ru = moderation_mapping.get(harmful.lower(), harmful)
377
+
378
+ processed_rows.append({
379
+ "Пользователь": user,
380
+ "Комментарий": comment,
381
+ "Дата": formatted_date,
382
+ "Тональность": sentiment_ru,
383
+ "Категория": category_ru,
384
+ "Модерация": moderation_ru,
385
+ "Автоответ": auto_answer
386
+ })
387
+
388
+ # Create dataframes
389
+ df_all = pd.DataFrame(processed_rows)
390
+ df_moderation = df_all[["Пользователь", "Комментарий", "Модерация"]].copy()
391
+ df_answers = df_all[["Пользователь", "Комментарий", "Автоответ"]].copy()
392
+
393
+ # Calculate statistics
394
+ total_comments = len(df_all)
395
+ unique_users = df_all["Пользователь"].nunique() if "Пользователь" in df_all.columns else 0
396
+ categories = df_all["Категория"].value_counts()
397
+ sentiments = df_all["Тональность"].value_counts()
398
+
399
+ # Create statistics markdown
400
+ stats_text = f"""
401
+ **📊 Общая статистика:**
402
+ - **Всего комментариев:** {total_comments}
403
+ - **Уникальных пользователей:** {unique_users}
404
+
405
+ **📂 По категориям:**
406
+ {chr(10).join([f'- **{category}:** {count}' for category, count in categories.items()])}
407
+
408
+ **💭 По тональности:**
409
+ {chr(10).join([f'- **{sentiment}:** {count}' for sentiment, count in sentiments.items()])}
410
+ """.strip()
411
+
412
+ # Create charts
413
+ fig_categories, fig_sentiments = make_charts(categories, sentiments)
414
+
415
+ # Create report files
416
+ print("Creating report files...")
417
+ report_files = create_report_files(df_all, categories, sentiments)
418
+ print(f"Report files created: {report_files}")
419
+
420
+ success_message = f"✅ Успешно обработано {total_comments} комментариев от {unique_users} пользователей!"
421
+
422
+ return (
423
+ success_message,
424
+ df_all,
425
+ df_moderation,
426
+ df_answers,
427
+ stats_text,
428
+ fig_categories,
429
+ fig_sentiments,
430
+ report_files
431
+ )
432
+
433
+ except Exception as e:
434
+ print(f"Data processing error: {e}")
435
+ return (
436
+ f"❌ Ошибка при обработке данных: {str(e)}",
437
+ empty_df, empty_moderation, empty_answers,
438
+ f"Ошибка обработки: {str(e)}",
439
+ None, None, []
440
+ )
441
+
442
+ # ===================== Custom CSS =====================
443
+ custom_css = """
444
+ /* Altel brand colors and styling */
445
+ .gradio-container {
446
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
447
+ max-width: 1200px;
448
+ margin: 0 auto;
449
+ }
450
+
451
+ /* Headers and titles */
452
+ .gradio-container h1, .gradio-container h2, .gradio-container h3 {
453
+ color: #E6007E !important;
454
+ font-weight: 600;
455
+ }
456
+
457
+ /* Primary buttons */
458
+ .gradio-container .primary {
459
+ background: linear-gradient(135deg, #E6007E 0%, #C5006C 100%) !important;
460
+ color: white !important;
461
+ border: none !important;
462
+ border-radius: 8px !important;
463
+ font-weight: 600 !important;
464
+ box-shadow: 0 2px 4px rgba(230, 0, 126, 0.3) !important;
465
+ transition: all 0.3s ease !important;
466
+ }
467
+
468
+ .gradio-container .primary:hover {
469
+ transform: translateY(-1px) !important;
470
+ box-shadow: 0 4px 8px rgba(230, 0, 126, 0.4) !important;
471
+ }
472
+
473
+ /* Tab styling */
474
+ .gradio-container .tab-nav button {
475
+ color: #E6007E !important;
476
+ border-bottom: 2px solid transparent !important;
477
+ }
478
+
479
+ .gradio-container .tab-nav button.selected {
480
+ color: #E6007E !important;
481
+ border-bottom: 2px solid #E6007E !important;
482
+ font-weight: 600 !important;
483
+ }
484
+
485
+ /* Table headers */
486
+ .gradio-container table thead th {
487
+ background-color: #E6007E !important;
488
+ color: white !important;
489
+ font-weight: 600 !important;
490
+ }
491
+
492
+ /* Cards and blocks */
493
+ .gradio-container .block {
494
+ border-radius: 12px !important;
495
+ border: 1px solid #f0f0f0 !important;
496
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important;
497
+ }
498
+
499
+ /* Status messages */
500
+ .gradio-container .textbox textarea[readonly] {
501
+ background-color: #f8f9fa !important;
502
+ border-left: 4px solid #E6007E !important;
503
+ }
504
+ """
505
+
506
+ # ===================== Gradio Interface =====================
507
+ def create_app():
508
+ """Create the Gradio application"""
509
+
510
+ with gr.Blocks(css=custom_css, title="Instagram Comment Analyzer", theme=gr.themes.Soft()) as app:
511
+
512
+ # Header
513
+ gr.Markdown("""
514
+ # 📸 Instagram Comment Analyzer
515
+
516
+ Анализ комментариев Instagram с помощью ИИ. Получите детальную аналитику тональности,
517
+ категоризацию и модерацию контента.
518
+
519
+ **Как использовать:**
520
+ 1. Вставьте ссылку на публичный пост Instagram
521
+ 2. Нажмите "Анализировать комментарии"
522
+ 3. Просмотрите результаты в различных вкладках
523
+ """)
524
+
525
+ # Input section
526
+ with gr.Row():
527
+ with gr.Column(scale=4):
528
+ url_input = gr.Textbox(
529
+ label="🔗 Instagram URL",
530
+ placeholder="https://www.instagram.com/p/XXXXXXXXX/",
531
+ info="Введите ссылку на пост, рилс или IGTV"
532
+ )
533
+ with gr.Column(scale=1):
534
+ analyze_btn = gr.Button(
535
+ "🚀 Анализировать комментарии",
536
+ variant="primary",
537
+ size="lg"
538
+ )
539
+
540
+ # Status output
541
+ status_output = gr.Textbox(
542
+ label="📋 Статус обработки",
543
+ interactive=False,
544
+ lines=2
545
+ )
546
+
547
+ # Results tabs
548
+ with gr.Tabs():
549
+ with gr.Tab("💬 Все комментарии"):
550
+ comments_df = gr.Dataframe(
551
+ headers=["Пользователь", "Комментарий", "Дата", "Тональность", "Категория", "Модерация", "Автоответ"],
552
+ interactive=False,
553
+ wrap=True
554
+ )
555
+
556
+ with gr.Tab("🛡️ Модерация"):
557
+ moderation_df = gr.Dataframe(
558
+ headers=["Пользователь", "Комментарий", "Модерация"],
559
+ interactive=False,
560
+ wrap=True
561
+ )
562
+
563
+ with gr.Tab("🤖 Автоответы"):
564
+ answers_df = gr.Dataframe(
565
+ headers=["Пользователь", "Комментарий", "Автоответ"],
566
+ interactive=False,
567
+ wrap=True
568
+ )
569
+ with gr.Tab("📊 Аналитика"):
570
+ with gr.Row():
571
+ stats_markdown = gr.Markdown("Загрузите Instagram ссылку для просмотра статистики.")
572
+
573
+ # Use Matplotlib components because make_charts returns matplotlib fig objects
574
+ with gr.Row():
575
+ with gr.Column():
576
+ categories_chart = gr.Plot(label="Распределение по категориям")
577
+ with gr.Column():
578
+ sentiments_chart = gr.Plot(label="Распределение по тональности")
579
+ # with gr.Column():
580
+ # categories_chart = gr.Matplotlib(label="Распределение по категориям")
581
+ # with gr.Column():
582
+ # sentiments_chart = gr.Matplotlib(label="Распределение по тональности")
583
+
584
+ download_files = gr.File(
585
+ label="📁 Скачать отчеты (CSV + Excel)",
586
+ file_count="multiple",
587
+ file_types=[".csv", ".xlsx"],
588
+ interactive=False,
589
+ visible=True
590
+ )
591
+
592
+ # Example section
593
+ gr.Markdown("""
594
+ ### 📝 Примеры ссылок:
595
+ - `https://www.instagram.com/p/XXXXXXXXX/` - обычный пост
596
+ - `https://www.instagram.com/reel/XXXXXXXXX/` - рилс
597
+ - `https://www.instagram.com/tv/XXXXXXXXX/` - IGTV
598
+
599
+ ⚠️ **Важно:** Ссылка должна вести на публичный контент
600
+
601
+ ### 🔧 Отладка:
602
+ Если файлы не скачиваются, проверьте логи в консоли Hugging Face Spaces.
603
+ """)
604
+
605
+ # Add test file generation button
606
+ with gr.Row():
607
+ test_files_btn = gr.Button("🧪 Создать тестовые файлы", variant="secondary")
608
+ test_status = gr.Textbox(label="Статус теста", interactive=False, visible=False)
609
+ test_files_output = gr.File(label="Тестовые файлы", file_count="multiple", visible=False)
610
+
611
+ # Connect the processing function
612
+ analyze_btn.click(
613
+ fn=process_instagram_url,
614
+ inputs=[url_input],
615
+ outputs=[
616
+ status_output,
617
+ comments_df,
618
+ moderation_df,
619
+ answers_df,
620
+ stats_markdown,
621
+ categories_chart,
622
+ sentiments_chart,
623
+ download_files
624
+ ]
625
+ )
626
+
627
+ # Connect test function
628
+ test_files_btn.click(
629
+ fn=test_file_generation,
630
+ inputs=[],
631
+ outputs=[test_status, test_files_output]
632
+ ).then(
633
+ lambda: (gr.update(visible=True), gr.update(visible=True)),
634
+ outputs=[test_status, test_files_output]
635
+ )
636
+
637
+ return app
638
+
639
+ # ===================== Launch Application =====================
640
+ if __name__ == "__main__":
641
+ app = create_app()
642
+ app.launch(
643
+ share=False,
644
+ server_name="0.0.0.0",
645
+ server_port=7860
646
+ )