Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sentence_transformers import SentenceTransformer, util
|
| 4 |
+
import openai
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# --- НАСТРОЙКИ СТРАНИЦЫ ---
|
| 8 |
+
st.set_page_config(layout="wide", page_title="SEO Intent Analyzer Pro")
|
| 9 |
+
|
| 10 |
+
# --- КЭШИРОВАНИЕ МОДЕЛИ (Чтобы не загружать 400мб каждый раз) ---
|
| 11 |
+
@st.cache_resource
|
| 12 |
+
def load_model():
|
| 13 |
+
return SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
model = load_model()
|
| 17 |
+
except Exception as e:
|
| 18 |
+
st.error(f"Ошибка загрузки модели: {e}")
|
| 19 |
+
|
| 20 |
+
# --- ИНТЕРФЕЙС: БОКОВАЯ ПАНЕЛЬ ---
|
| 21 |
+
st.sidebar.title("⚙️ Настройки")
|
| 22 |
+
|
| 23 |
+
language = st.sidebar.selectbox(
|
| 24 |
+
"Язык анализа",
|
| 25 |
+
["English", "Hindi", "Spanish", "Bengali"]
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
api_key = st.sidebar.text_input("OpenAI API Key (для рекомендаций)", type="password")
|
| 29 |
+
target_keyword = st.sidebar.text_input("Целевой Интент / Ключ", "online casino guide")
|
| 30 |
+
|
| 31 |
+
st.sidebar.markdown("---")
|
| 32 |
+
st.sidebar.info("Загрузите тексты конкурентов и свой текст, затем нажмите 'Анализировать'.")
|
| 33 |
+
|
| 34 |
+
# --- ИНТЕРФЕЙС: ОСНОВНАЯ ЧАСТЬ ---
|
| 35 |
+
st.title("🚀 Анализатор Интента (SBERT + AI)")
|
| 36 |
+
|
| 37 |
+
col1, col2 = st.columns(2)
|
| 38 |
+
|
| 39 |
+
with col1:
|
| 40 |
+
st.subheader("📝 Ваш Текст")
|
| 41 |
+
user_text = st.text_area("Вставьте ваш контент сюда", height=400)
|
| 42 |
+
|
| 43 |
+
with col2:
|
| 44 |
+
st.subheader("🕵️ Конкуренты")
|
| 45 |
+
# Используем табы для конкурентов, чтобы не загромождать экран
|
| 46 |
+
tabs = st.tabs([f"Конкурент {i+1}" for i in range(5)])
|
| 47 |
+
competitors = []
|
| 48 |
+
|
| 49 |
+
for i, tab in enumerate(tabs):
|
| 50 |
+
with tab:
|
| 51 |
+
comp_text = st.text_area(f"Текст конкурента {i+1}", height=320, key=f"comp_{i}")
|
| 52 |
+
if len(comp_text) > 50:
|
| 53 |
+
competitors.append(comp_text)
|
| 54 |
+
|
| 55 |
+
# --- ЛОГИКА АНАЛИЗА ---
|
| 56 |
+
if st.button("🚀 ЗАПУСТИТЬ АНАЛИЗ", type="primary"):
|
| 57 |
+
if not user_text:
|
| 58 |
+
st.warning("Пожалуйста, введите ваш текст.")
|
| 59 |
+
elif not competitors:
|
| 60 |
+
st.warning("Пожалуйста, добавьте хотя бы одного конкурента.")
|
| 61 |
+
else:
|
| 62 |
+
with st.spinner('Загрузка нейросети и сравнение векторов...'):
|
| 63 |
+
# 1. Анализ SBERT
|
| 64 |
+
user_emb = model.encode(user_text, convert_to_tensor=True)
|
| 65 |
+
comp_embs = [model.encode(c, convert_to_tensor=True) for c in competitors]
|
| 66 |
+
|
| 67 |
+
# Средний вектор конкурентов
|
| 68 |
+
avg_comp_emb = np.mean([c.cpu().numpy() for c in comp_embs], axis=0)
|
| 69 |
+
|
| 70 |
+
# Сходство
|
| 71 |
+
similarity = util.cos_sim(user_emb, avg_comp_emb).item() * 100
|
| 72 |
+
|
| 73 |
+
# 2. Поиск упущенных аспектов (Gap Analysis)
|
| 74 |
+
user_sentences = user_text.split('.')
|
| 75 |
+
# Собираем все предложения конкурентов в кучу
|
| 76 |
+
comp_sentences_flat = []
|
| 77 |
+
for c in competitors:
|
| 78 |
+
comp_sentences_flat.extend([s for s in c.split('.') if len(s) > 30])
|
| 79 |
+
|
| 80 |
+
# Кодируем предложения
|
| 81 |
+
user_sent_embs = model.encode(user_sentences, convert_to_tensor=True)
|
| 82 |
+
comp_sent_embs = model.encode(comp_sentences_flat, convert_to_tensor=True)
|
| 83 |
+
|
| 84 |
+
# Матрица схожести
|
| 85 |
+
cos_scores = util.cos_sim(comp_sent_embs, user_sent_embs)
|
| 86 |
+
|
| 87 |
+
missing_aspects = []
|
| 88 |
+
# Если предложение конкурента не похоже ни на одно наше (score < 0.45)
|
| 89 |
+
for idx, scores in enumerate(cos_scores):
|
| 90 |
+
if scores.max() < 0.45:
|
| 91 |
+
missing_aspects.append(comp_sentences_flat[idx].strip())
|
| 92 |
+
|
| 93 |
+
# Убираем дубли и берем топ-5
|
| 94 |
+
missing_aspects = list(set(missing_aspects))[:5]
|
| 95 |
+
|
| 96 |
+
# 3. AI Рекомендации
|
| 97 |
+
recommendations = []
|
| 98 |
+
if api_key and missing_aspects:
|
| 99 |
+
openai.api_key = api_key
|
| 100 |
+
status_text = st.empty()
|
| 101 |
+
status_text.text("Генерация рекомендаций через GPT...")
|
| 102 |
+
|
| 103 |
+
for aspect in missing_aspects:
|
| 104 |
+
prompt = f"""
|
| 105 |
+
Role: Expert SEO Copywriter.
|
| 106 |
+
Language: {language}.
|
| 107 |
+
Task: My competitors mention: "{aspect}", but I missed it.
|
| 108 |
+
Action: Write a paragraph (HTML format) to insert into my article to cover this topic.
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
response = openai.ChatCompletion.create(
|
| 112 |
+
model="gpt-3.5-turbo",
|
| 113 |
+
messages=[{"role": "user", "content": prompt}],
|
| 114 |
+
max_tokens=300
|
| 115 |
+
)
|
| 116 |
+
recommendations.append(response.choices[0].message.content)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
st.error(f"AI Error: {e}")
|
| 119 |
+
status_text.empty()
|
| 120 |
+
|
| 121 |
+
# --- ВЫВОД РЕЗУЛЬТАТОВ ---
|
| 122 |
+
st.divider()
|
| 123 |
+
st.header("📊 Результаты Анализа")
|
| 124 |
+
|
| 125 |
+
# Метрики
|
| 126 |
+
m1, m2, m3 = st.columns(3)
|
| 127 |
+
m1.metric("Сходство с Топом", f"{similarity:.1f}%")
|
| 128 |
+
m2.metric("Длина вашего текста", f"{len(user_text.split())} слов")
|
| 129 |
+
m3.metric("Среднее у конкурентов", f"{int(np.mean([len(c.split()) for c in competitors]))} слов")
|
| 130 |
+
|
| 131 |
+
# Визуализация прогресса
|
| 132 |
+
st.write("Шкала раскрытия интента:")
|
| 133 |
+
st.progress(min(int(similarity), 100))
|
| 134 |
+
|
| 135 |
+
# Таблица сравнения
|
| 136 |
+
col_res1, col_res2 = st.columns(2)
|
| 137 |
+
|
| 138 |
+
with col_res1:
|
| 139 |
+
st.subheader("⚠️ Упущенные аспекты (Gaps)")
|
| 140 |
+
if missing_aspects:
|
| 141 |
+
for gap in missing_aspects:
|
| 142 |
+
st.error(f"У конкурентов: \"{gap}\"")
|
| 143 |
+
else:
|
| 144 |
+
st.success("Явных пробелов в интенте не найдено!")
|
| 145 |
+
|
| 146 |
+
with col_res2:
|
| 147 |
+
st.subheader("💡 Рекомендации к внедрению")
|
| 148 |
+
if recommendations:
|
| 149 |
+
for rec in recommendations:
|
| 150 |
+
st.code(rec, language="html")
|
| 151 |
+
st.caption("Скопируйте код выше и вставьте в статью")
|
| 152 |
+
elif not api_key:
|
| 153 |
+
st.info("Введите API Key слева, чтобы получить готовый текст исправлений.")
|
| 154 |
+
else:
|
| 155 |
+
st.info("Рекомендации не требуются.")
|
| 156 |
+
|
| 157 |
+
# Экспорт
|
| 158 |
+
report_text = f"""
|
| 159 |
+
<h1>SEO Report - {target_keyword}</h1>
|
| 160 |
+
<p>Date: {datetime.now()}</p>
|
| 161 |
+
<p>Similarity: {similarity:.2f}%</p>
|
| 162 |
+
<h2>Missing Aspects:</h2>
|
| 163 |
+
<ul>{''.join([f'<li>{m}</li>' for m in missing_aspects])}</ul>
|
| 164 |
+
<h2>AI Recommendations:</h2>
|
| 165 |
+
{''.join(recommendations)}
|
| 166 |
+
"""
|
| 167 |
+
st.download_button(
|
| 168 |
+
label="📥 Скачать HTML отчет",
|
| 169 |
+
data=report_text,
|
| 170 |
+
file_name="seo_report.html",
|
| 171 |
+
mime="text/html"
|
| 172 |
+
)
|
| 173 |
+
|