lsdf commited on
Commit
e0ad138
·
0 Parent(s):

Initial commit: SEO AI Editor MVP with BERT, BM25 and N-gram analysis

Browse files
Files changed (11) hide show
  1. .gitignore +55 -0
  2. README.md +253 -0
  3. docs/API.md +257 -0
  4. docs/ARCHITECTURE.md +290 -0
  5. docs/DEVELOPMENT.md +333 -0
  6. logic.py +464 -0
  7. main.py +62 -0
  8. models.py +19 -0
  9. ps.sh +2 -0
  10. requirements.txt +10 -0
  11. templates/index.html +427 -0
.gitignore ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ env/
26
+ ENV/
27
+ env.bak/
28
+ venv.bak/
29
+
30
+ # IDE
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+ .DS_Store
37
+
38
+ # Environment variables
39
+ .env
40
+ .env.local
41
+
42
+ # Temporary files
43
+ pip_temp/
44
+ *.log
45
+ *.tmp
46
+
47
+ # Jupyter Notebook
48
+ .ipynb_checkpoints
49
+
50
+ # PyTorch models cache (опционально, если хотите кэшировать модели локально)
51
+ # .cache/
52
+
53
+ # OS
54
+ Thumbs.db
55
+ .DS_Store
README.md ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SEO AI Editor
2
+
3
+ Веб-приложение для анализа и оптимизации SEO-текстов с использованием искусственного интеллекта. Проект использует лингвистический анализ (spaCy), частотный анализ (BM25) и семантический анализ (BERT) для сравнения вашего текста с текстами конкурентов.
4
+
5
+ ## 🚀 Возможности
6
+
7
+ - **Многоязычный анализ**: Поддержка русского, английского, немецкого, испанского и итальянского языков
8
+ - **N-граммный анализ**: Статистика по униграммам, биграммам, триграммам и квадриграммам
9
+ - **BM25 рекомендации**: Автоматические рекомендации по добавлению/удалению слов и фраз
10
+ - **BERT семантический анализ**: Глубокий анализ релевантности текста ключевым фразам с использованием нейронных сетей
11
+ - **Сравнение с конкурентами**: Детальное сравнение вашего текста с текстами конкурентов
12
+ - **GPU ускорение**: Автоматическое использование GPU для ускорения BERT-анализа
13
+
14
+ ## 📋 Требования
15
+
16
+ - Python 3.8+
17
+ - CUDA (опционально, для GPU ускорения)
18
+ - 4+ GB RAM (рекомендуется 8+ GB)
19
+
20
+ ## 🔧 Установка
21
+
22
+ 1. Клонируйте репозиторий или перейдите в папку проекта:
23
+ ```bash
24
+ cd seo_ai_editor
25
+ ```
26
+
27
+ 2. Создайте виртуальное окружение:
28
+ ```bash
29
+ python -m venv venv
30
+ ```
31
+
32
+ 3. Активируйте виртуальное окружение:
33
+ - Windows:
34
+ ```bash
35
+ venv\Scripts\activate
36
+ ```
37
+ - Linux/Mac:
38
+ ```bash
39
+ source venv/bin/activate
40
+ ```
41
+
42
+ 4. Установите зависимости:
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ 5. Установите языковые модели spaCy:
48
+ ```bash
49
+ python -m spacy download en_core_web_sm
50
+ python -m spacy download ru_core_news_sm
51
+ python -m spacy download de_core_news_sm
52
+ python -m spacy download es_core_news_sm
53
+ python -m spacy download it_core_news_sm
54
+ ```
55
+
56
+ ## 🏃 Запуск
57
+
58
+ Запустите приложение:
59
+ ```bash
60
+ python main.py
61
+ ```
62
+
63
+ Или используйте uvicorn напрямую:
64
+ ```bash
65
+ uvicorn main:app --host 127.0.0.1 --port 8001 --reload
66
+ ```
67
+
68
+ Приложение будет доступно по адресу: `http://127.0.0.1:8001`
69
+
70
+ ## 📖 Использование
71
+
72
+ 1. Откройте браузер и перейдите на `http://127.0.0.1:8001`
73
+ 2. Выберите язык анализа
74
+ 3. Введите ваш текст в поле "Ваш текст (Target)"
75
+ 4. Введите ключевые фразы (каждая фраза с новой строки)
76
+ 5. Добавьте тексты конкурентов (можно добавить несколько)
77
+ 6. Нажмите кнопку "⚡ Анализировать (GPU)"
78
+
79
+ ### Интерфейс результатов
80
+
81
+ Приложение предоставляет три вкладки с результатами:
82
+
83
+ #### 🧠 BERT Семантика
84
+ - **Общий рейтинг релевантности**: Сравнение вашего текста и конкурентов по среднему сходству с ключевыми фразами
85
+ - **Детальный анализ**: Для каждой ключевой фразы показывается:
86
+ - Максимальный score в вашем тексте
87
+ - Максимальный score у конкурентов
88
+ - Топ-5 наиболее релевантных предложений из вашего текста
89
+ - Топ-5 наиболее релевантных предложений у конкурентов
90
+ - Рекомендации по улучшению
91
+
92
+ #### 📊 BM25 Баланс
93
+ - Рекомендации по добавлению/удалению слов и фраз
94
+ - Показывает частотные различия между вашим текстом и конкурентами
95
+ - **Полная декомпозиция фраз**: Автоматически анализирует все возможные комбинации слов из ключевых фраз
96
+ - Например, для фразы "chicken road casino" анализируются: "chicken", "road", "casino", "chicken road", "road casino", "chicken road casino"
97
+ - Учитывает униграммы, биграммы и триграммы
98
+ - Умная сортировка: сначала показываются проблемные рекомендации, затем по длине фразы
99
+
100
+ #### 🔠 N-граммы
101
+ - Детальная статистика по частоте слов и фраз
102
+ - Сравнение с конкурентами
103
+ - Подсветка важных различий
104
+
105
+ ## 🏗️ Архитектура проекта
106
+
107
+ ```
108
+ seo_ai_editor/
109
+ ├── main.py # FastAPI приложение и роутинг
110
+ ├── logic.py # Основная бизнес-логика анализа
111
+ ├── models.py # Pydantic модели для API
112
+ ├── requirements.txt # Зависимости проекта
113
+ ├── templates/
114
+ │ └── index.html # Frontend интерфейс
115
+ └── README.md # Документация
116
+ ```
117
+
118
+ ### Основные компоненты
119
+
120
+ #### `main.py`
121
+ - FastAPI приложение
122
+ - Роутинг: `/` (главная страница) и `/analyze` (API анализа)
123
+ - Предзагрузка моделей при старте
124
+
125
+ #### `logic.py`
126
+ Содержит три основных модуля:
127
+
128
+ 1. **SPACY (Лингвистика)**
129
+ - `load_models()` - загрузка языковых моделей
130
+ - `get_doc()` - обработка текста через spaCy
131
+ - `get_lemmas_flat()` - получение лемматизированных токенов
132
+ - `generate_ngrams_safe()` - генерация N-грамм
133
+
134
+ 2. **ANALYTICS (N-grams & BM25)**
135
+ - `calculate_ngram_stats()` - статистика N-грамм
136
+ - `parse_keywords()` - парсинг ключевых фраз
137
+ - `calculate_bm25_recommendations()` - BM25 рекомендации
138
+
139
+ 3. **BERT / VECTOR ANALYSIS**
140
+ - `get_bert_model()` - загрузка BERT модели
141
+ - `perform_bert_analysis()` - семантический анализ
142
+
143
+ #### `models.py`
144
+ Pydantic модели:
145
+ - `AnalysisRequest` - запрос на анализ
146
+ - `AnalysisResponse` - ответ с результатами анализа
147
+
148
+ ## 🔬 Технологии
149
+
150
+ - **FastAPI** - веб-фреймворк
151
+ - **spaCy** - лингвистический анализ
152
+ - **rank-bm25** - алгоритм BM25 для ранжирования
153
+ - **sentence-transformers** - BERT модели для семантического анализа
154
+ - **PyTorch** - глубокое обучение (для BERT)
155
+ - **Jinja2** - шаблонизация HTML
156
+ - **Bootstrap 5** - UI фреймворк
157
+
158
+ ## 📝 API Документация
159
+
160
+ После запуска приложения доступна автоматическая документация API:
161
+ - Swagger UI: `http://127.0.0.1:8001/docs`
162
+ - ReDoc: `http://127.0.0.1:8001/redoc`
163
+
164
+ ### Endpoint: `/analyze`
165
+
166
+ **Метод:** POST
167
+
168
+ **Тело запроса:**
169
+ ```json
170
+ {
171
+ "target_text": "Ваш текст для анализа",
172
+ "competitors": ["Текст конкурента 1", "Текст конкурента 2"],
173
+ "keywords": ["ключевая фраза 1", "ключевая фраза 2"],
174
+ "language": "ru"
175
+ }
176
+ ```
177
+
178
+ **Ответ:**
179
+ ```json
180
+ {
181
+ "ngram_stats": {
182
+ "unigrams": [...],
183
+ "bigrams": [...],
184
+ "trigrams": [...],
185
+ "quadgrams": [...]
186
+ },
187
+ "bm25_recommendations": [
188
+ {
189
+ "word": "слово",
190
+ "type": "1-gram",
191
+ "my_score": 2.5,
192
+ "avg_comp_score": 3.0,
193
+ "action": "add",
194
+ "count": 2
195
+ }
196
+ ],
197
+ "bert_analysis": {
198
+ "global_scores": [...],
199
+ "detailed": [...]
200
+ }
201
+ }
202
+ ```
203
+
204
+ ## ⚙️ Настройки
205
+
206
+ ### Языки
207
+ Поддерживаемые языки задаются в `logic.py` в словаре `MODEL_NAMES`:
208
+ - `en` - английский
209
+ - `ru` - русский
210
+ - `de` - немецкий
211
+ - `es` - испанский
212
+ - `it` - итальянский
213
+
214
+ ### BERT модель
215
+ По умолчанию используется `paraphrase-multilingual-MiniLM-L12-v2`. Модель можно изменить в функции `get_bert_model()` в файле `logic.py`.
216
+
217
+ ### Пороги BM25
218
+ Пороги для рекомендаций можно настроить в функции `calculate_bm25_recommendations()`:
219
+ - Униграммы: `threshold = 0.5`
220
+ - Биграммы: `threshold = 0.25`
221
+ - Триграммы: `threshold = 0.15`
222
+
223
+ **Примечание:** Алгоритм BM25 автоматически выполняет полную декомпозицию ключевых фраз на все возможные под-н-граммы (1-3 слова). Это позволяет находить не только точные совпадения, но и частичные вхождения ключевых фраз в тексте.
224
+
225
+ ## 🐛 Решение проблем
226
+
227
+ ### Ошибка загрузки spaCy модели
228
+ Убедитесь, что модель установлена:
229
+ ```bash
230
+ python -m spacy download <model_name>
231
+ ```
232
+
233
+ ### Медленная работа BERT
234
+ - Убедитесь, что CUDA установлена и доступна (��ля GPU ускорения)
235
+ - При первом запуске модель загружается, это может занять время
236
+ - Используйте GPU для значительного ускорения
237
+
238
+ ### Проблемы с памятью
239
+ - Уменьшите количество конкурентов
240
+ - Разбейте длинные тексты на части
241
+ - Используйте более легкую BERT модель
242
+
243
+ ## 📄 Лицензия
244
+
245
+ Проект создан для образовательных и коммерческих целей.
246
+
247
+ ## 🤝 Вклад
248
+
249
+ Приветствуются улучшения и предложения! Создавайте issues и pull requests.
250
+
251
+ ## 📧 Контакты
252
+
253
+ Для вопросов и предложений создавайте issues в репозитории проекта.
docs/API.md ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Документация
2
+
3
+ ## Обзор
4
+
5
+ SEO AI Editor предоставляет REST API для анализа текстов. API построен на FastAPI и автоматически генерирует интерактивную документацию.
6
+
7
+ ## Базовый URL
8
+
9
+ ```
10
+ http://127.0.0.1:8001
11
+ ```
12
+
13
+ ## Endpoints
14
+
15
+ ### GET `/`
16
+
17
+ Возвращает главную страницу приложения (HTML).
18
+
19
+ **Ответ:** HTML страница с интерфейсом
20
+
21
+ ---
22
+
23
+ ### POST `/analyze`
24
+
25
+ Выполняет комплексный анализ текста с использованием N-грамм, BM25 и BERT.
26
+
27
+ #### Запрос
28
+
29
+ **Content-Type:** `application/json`
30
+
31
+ **Тело запроса:**
32
+
33
+ ```json
34
+ {
35
+ "target_text": "string (обязательно)",
36
+ "competitors": ["string"] (обязательно, может быть пустым массивом),
37
+ "keywords": ["string"] (обязательно, может быть пустым массивом),
38
+ "language": "string" (опционально, по умолчанию "en")
39
+ }
40
+ ```
41
+
42
+ **Параметры:**
43
+
44
+ | Параметр | Тип | Обязательный | Описание |
45
+ |----------|-----|--------------|----------|
46
+ | `target_text` | string | Да | Текст пользователя для анализа |
47
+ | `competitors` | array[string] | Да | Массив текстов конкурентов |
48
+ | `keywords` | array[string] | Да | Массив ключевых фраз (каждая фраза - отдельный элемент) |
49
+ | `language` | string | Нет | Код языка: `en`, `ru`, `de`, `es`, `it` |
50
+
51
+ #### Ответ
52
+
53
+ **Статус:** 200 OK
54
+
55
+ **Content-Type:** `application/json`
56
+
57
+ ```json
58
+ {
59
+ "ngram_stats": {
60
+ "unigrams": [
61
+ {
62
+ "ngram": "string",
63
+ "target_count": 0,
64
+ "competitor_avg": 0.0
65
+ }
66
+ ],
67
+ "bigrams": [...],
68
+ "trigrams": [...],
69
+ "quadgrams": [...]
70
+ },
71
+ "bm25_recommendations": [
72
+ {
73
+ "word": "string",
74
+ "type": "1-gram" | "2-gram" | "3-gram",
75
+ "my_score": 0.0,
76
+ "avg_comp_score": 0.0,
77
+ "action": "ok" | "add" | "remove",
78
+ "count": 0
79
+ }
80
+ ],
81
+ "bert_analysis": {
82
+ "global_scores": [
83
+ {
84
+ "name": "string",
85
+ "score": 0.0,
86
+ "is_me": true
87
+ }
88
+ ],
89
+ "detailed": [
90
+ {
91
+ "phrase": "string",
92
+ "my_max_score": 0.0,
93
+ "comp_max_score": 0.0,
94
+ "status": "ok" | "good" | "warning" | "bad",
95
+ "recommendation": "string",
96
+ "my_top_chunks": [
97
+ {
98
+ "text": "string",
99
+ "score": 0.0
100
+ }
101
+ ],
102
+ "comp_top_chunks": [
103
+ {
104
+ "text": "string",
105
+ "score": 0.0,
106
+ "source": "string"
107
+ }
108
+ ]
109
+ }
110
+ ]
111
+ }
112
+ }
113
+ ```
114
+
115
+ #### Структура ответа
116
+
117
+ ##### ngram_stats
118
+
119
+ Статистика по N-граммам (1-4 слова).
120
+
121
+ **Поля:**
122
+ - `ngram` - текст N-граммы (лемматизированный)
123
+ - `target_count` - количество вхождений в целевом тексте
124
+ - `competitor_avg` - среднее количество вхождений у конкурентов
125
+
126
+ **Сортировка:** По максимальному значению (target_count или competitor_avg)
127
+
128
+ ##### bm25_recommendations
129
+
130
+ Рекомендации по оптимизации частоты слов/фраз с использованием алгоритма BM25.
131
+
132
+ **Особенности алгоритма:**
133
+ - **Полная декомпозиция фраз**: Каждая ключевая фраза автоматически разбивается на все возможные под-н-граммы длиной от 1 до 3 слов
134
+ - Пример: фраза "chicken road casino" анализируется как:
135
+ - Униграммы: "chicken", "road", "casino"
136
+ - Биграммы: "chicken road", "road casino"
137
+ - Триграммы: "chicken road casino"
138
+ - Это позволяет находить не только точные совпадения, но и частичные вхождения ключевых фраз
139
+ - Дубликаты автоматически удаляются
140
+
141
+ **Поля:**
142
+ - `word` - слово или фраза (лемматизированная)
143
+ - `type` - тип: "1-gram", "2-gram", "3-gram"
144
+ - `my_score` - BM25 score в целевом тексте
145
+ - `avg_comp_score` - средний BM25 score у конкурентов
146
+ - `action` - рекомендуемое действие:
147
+ - `"ok"` - частота в норме
148
+ - `"add"` - нужно добавить (ваш score ниже среднего конкурентов)
149
+ - `"remove"` - нужно убрать (ваш score значительно выше среднего конкурентов)
150
+ - `count` - рекомендуемое количество добавлений/удалений (рассчитывается на основе разницы скоров)
151
+
152
+ **Пороги для действий:**
153
+ - Униграммы: порог 0.5
154
+ - Биграммы: порог 0.25
155
+ - Триграммы: порог 0.15
156
+
157
+ **Сортировка:**
158
+ 1. Сначала проблемные рекомендации (add/remove)
159
+ 2. Затем по длине фразы (длинные фразы важнее)
160
+ 3. Затем алфавитно
161
+
162
+ ##### bert_analysis
163
+
164
+ Семантический анализ с использованием BERT.
165
+
166
+ **global_scores:**
167
+ - `name` - название текста ("Мой текст" или "Конкурент #N")
168
+ - `score` - средний максимальный score по всем ключевым фразам (0.0 - 1.0)
169
+ - `is_me` - флаг, является ли это целевым текстом
170
+
171
+ **detailed:**
172
+ Для каждой ключевой фразы:
173
+ - `phrase` - исходная ключевая фраза
174
+ - `my_max_score` - максимальный score в целевом тексте (0.0 - 1.0)
175
+ - `comp_max_score` - максимальный score у конкурентов
176
+ - `status` - статус:
177
+ - `"good"` - score >= 0.7
178
+ - `"ok"` - 0.5 <= score < 0.7
179
+ - `"warning"` - score < 0.5 или конкуренты лучше на 0.1+
180
+ - `"bad"` - score < 0.5
181
+ - `recommendation` - текстовое описание рекомендации
182
+ - `my_top_chunks` - топ-5 наиболее релевантных предложений из целевого текста
183
+ - `comp_top_chunks` - топ-5 наиболее релевантных предложений у конкурентов (с указанием источника)
184
+
185
+ #### Примеры запросов
186
+
187
+ **cURL:**
188
+ ```bash
189
+ curl -X POST "http://127.0.0.1:8001/analyze" \
190
+ -H "Content-Type: application/json" \
191
+ -d '{
192
+ "target_text": "Это мой текст для анализа SEO.",
193
+ "competitors": ["Текст конкурента номер один.", "Текст конкурента номер два."],
194
+ "keywords": ["SEO анализ", "текст"],
195
+ "language": "ru"
196
+ }'
197
+ ```
198
+
199
+ **Python:**
200
+ ```python
201
+ import requests
202
+
203
+ response = requests.post(
204
+ "http://127.0.0.1:8001/analyze",
205
+ json={
206
+ "target_text": "Это мой текст для анализа SEO.",
207
+ "competitors": ["Текст конкурента номер один.", "Текст конкурента номер два."],
208
+ "keywords": ["SEO анализ", "текст"],
209
+ "language": "ru"
210
+ }
211
+ )
212
+
213
+ data = response.json()
214
+ print(data)
215
+ ```
216
+
217
+ **JavaScript:**
218
+ ```javascript
219
+ fetch('http://127.0.0.1:8001/analyze', {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ },
224
+ body: JSON.stringify({
225
+ target_text: "Это мой текст для анализа SEO.",
226
+ competitors: ["Текст конкурента номер один.", "Текст конкурента номер два."],
227
+ keywords: ["SEO анализ", "текст"],
228
+ language: "ru"
229
+ })
230
+ })
231
+ .then(response => response.json())
232
+ .then(data => console.log(data));
233
+ ```
234
+
235
+ #### Ошибки
236
+
237
+ **400 Bad Request**
238
+ Неверный формат запроса или отсутствуют обязательные поля.
239
+
240
+ **422 Unprocessable Entity**
241
+ Ошибка валидации данных (например, неверный код языка).
242
+
243
+ **500 Internal Server Error**
244
+ Внутренняя ошибка сервера (проблемы с моделями, памятью и т.д.).
245
+
246
+ ## Интерактивная документация
247
+
248
+ После запуска приложения доступны:
249
+
250
+ - **Swagger UI**: `http://127.0.0.1:8001/docs`
251
+ - **ReDoc**: `http://127.0.0.1:8001/redoc`
252
+
253
+ Эти интерфейсы позволяют:
254
+ - Просматривать все endpoints
255
+ - Тестировать API прямо в браузере
256
+ - Видеть схемы данных
257
+ - Просматривать примеры запросов и ответов
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Архитектура проекта
2
+
3
+ ## Обзор
4
+
5
+ SEO AI Editor построен на архитектуре клиент-сервер с использованием FastAPI для backend и простого HTML/JavaScript для frontend.
6
+
7
+ ## Структура проекта
8
+
9
+ ```
10
+ seo_ai_editor/
11
+ ├── main.py # Точка входа, FastAPI приложение
12
+ ├── logic.py # Бизнес-логика и алгоритмы анализа
13
+ ├── models.py # Pydantic модели данных
14
+ ├── requirements.txt # Python зависимости
15
+ ├── templates/
16
+ │ └── index.html # Frontend интерфейс
17
+ ├── docs/ # Документация
18
+ │ ├── API.md
19
+ │ ├── ARCHITECTURE.md
20
+ │ └── DEVELOPMENT.md
21
+ └── README.md # Основная документация
22
+ ```
23
+
24
+ ## Компоненты системы
25
+
26
+ ### 1. Backend (FastAPI)
27
+
28
+ #### `main.py` - Веб-сервер
29
+
30
+ **Ответственность:**
31
+ - Инициализация FastAPI приложения
32
+ - Роутинг HTTP запросов
33
+ - Предзагрузка моделей при старте
34
+ - Обработка запросов и формирование ответов
35
+
36
+ **Ключевые функции:**
37
+ - `startup_event()` - загрузка моделей при старте
38
+ - `read_root()` - отдача главной страницы
39
+ - `analyze_text()` - обработка запроса на анализ
40
+
41
+ #### `logic.py` - Бизнес-логика
42
+
43
+ Разделен на три модуля:
44
+
45
+ ##### A. SPACY (Лингвистический анализ)
46
+
47
+ **Модели:**
48
+ - Глобальный словарь `LoadedModels` для кэширования загруженных spaCy моделей
49
+ - Поддержка 5 языков: en, ru, de, es, it
50
+
51
+ **Функции:**
52
+ - `load_model_if_missing(lang)` - ленивая загрузка моделей
53
+ - `load_models()` - предзагрузка всех моделей
54
+ - `get_doc(text, lang)` - получение spaCy документа
55
+ - `is_valid_token(t)` - фильтрация токенов (удаление мусора)
56
+ - `get_lemmas_flat(text, lang)` - получение списка лемм
57
+ - `generate_ngrams_safe(text, lang, n)` - генерация N-грамм с умной фильтрацией
58
+
59
+ **Особенности:**
60
+ - Сохранение стоп-слов внутри фраз для читаемости
61
+ - Фильтрация N-грамм, состоящих только из стоп-слов
62
+ - Обработка больших текстов (max_length = 2,000,000)
63
+
64
+ ##### B. ANALYTICS (N-граммы и BM25)
65
+
66
+ **Функции:**
67
+ - `calculate_ngram_stats()` - статистика по N-граммам (1-4)
68
+ - `parse_keywords()` - парсинг ключевых фраз
69
+ - `calculate_bm25_recommendations()` - многоуровневый BM25 анализ
70
+
71
+ **Алгоритм BM25 (с полной декомпозицией фраз):**
72
+ 1. **Декомпозиция ключевых фраз**: Для каждой ключевой фразы генерируются все возможные под-н-граммы длиной от 1 до 3 слов
73
+ - Пример: фраза "chicken road casino" разбивается на:
74
+ - Униграммы: "chicken", "road", "casino"
75
+ - Биграммы: "chicken road", "road casino"
76
+ - Триграммы: "chicken road casino"
77
+ - Используется скользящее окно по токенам фразы
78
+ - Дубликаты отслеживаются через set для оптимизации
79
+ 2. Генерация N-грамм для целевого текста и конкурентов (униграммы, биграммы, триграммы)
80
+ 3. Обучение BM25 модели на корпусе N-грамм для каждого уровня (1, 2, 3)
81
+ 4. Расчет BM25 скоров для каждой декомпозированной фразы
82
+ 5. Сравнение скоров целевого текста со средним скором конкурентов
83
+ 6. Генерация рекомендаций (add/remove/ok) на основе пороговых значений
84
+ 7. Сортировка результатов: сначала проблемные (add/remove), затем по длине фразы, затем алфавитно
85
+
86
+ **Пороги:**
87
+ - Униграммы: 0.5
88
+ - Биграммы: 0.25
89
+ - Триграммы: 0.15
90
+
91
+ **Особенности:**
92
+ - Полная декомпозиция позволяет анализировать не только целые фразы, но и их части
93
+ - Это особенно полезно для длинных ключевых фраз, которые могут встречаться в тексте частично
94
+ - Автоматическое удаление дубликатов при декомпозиции
95
+
96
+ ##### C. BERT / VECTOR ANALYSIS
97
+
98
+ **Модель:**
99
+ - Глобальная переменная `BertModel` для кэширования
100
+ - Модель: `paraphrase-multilingual-MiniLM-L12-v2`
101
+ - Автоматическое определение устройства (CPU/GPU)
102
+
103
+ **Функции:**
104
+ - `get_bert_model()` - загрузка BERT модели
105
+ - `perform_bert_analysis()` - семантический анализ
106
+
107
+ **Алгоритм BERT анализа:**
108
+ 1. Разбиение текстов на предложения (chunks)
109
+ 2. Генерация эмбеддингов для всех chunks и ключевых фраз
110
+ 3. Расчет косинусного сходства между ключевыми фразами и chunks
111
+ 4. Global Score: средний максимальный score по всем ключам
112
+ 5. Detailed Analysis: топ-5 наиболее релевантных chunks для каждой фразы
113
+
114
+ #### `models.py` - Модели данных
115
+
116
+ **Pydantic модели:**
117
+ - `AnalysisRequest` - входные данные для анализа
118
+ - `AnalysisResponse` - структура ответа API
119
+
120
+ ### 2. Frontend
121
+
122
+ #### `templates/index.html`
123
+
124
+ **Технологии:**
125
+ - Bootstrap 5 для UI
126
+ - Vanilla JavaScript (без фреймворков)
127
+ - AJAX для взаимодействия с API
128
+
129
+ **Компоненты:**
130
+ - Форма ввода данных
131
+ - Табы для отображения результатов
132
+ - Динамическое добавление полей конкурентов
133
+ - Визуализация результатов анализа
134
+
135
+ ## Поток данных
136
+
137
+ ```
138
+ 1. Пользователь вводит данные в форму
139
+
140
+ 2. JavaScript собирает данные и отправляет POST /analyze
141
+
142
+ 3. FastAPI получает запрос, валидирует через Pydantic
143
+
144
+ 4. main.py вызывает функции из logic.py:
145
+ - calculate_ngram_stats()
146
+ - parse_keywords()
147
+ - calculate_bm25_recommendations()
148
+ - perform_bert_analysis()
149
+
150
+ 5. Каждая функция использует:
151
+ - spaCy для лингвистики
152
+ - BM25 для частотного анализа
153
+ - BERT для семантики
154
+
155
+ 6. Результаты собираются в AnalysisResponse
156
+
157
+ 7. JSON ответ отправляется клиенту
158
+
159
+ 8. JavaScript рендерит результаты в UI
160
+ ```
161
+
162
+ ## Управление состоянием
163
+
164
+ ### Backend
165
+
166
+ **Глобальные переменные:**
167
+ - `LoadedModels` - кэш загруженных spaCy моделей
168
+ - `BertModel` - кэш BERT модели
169
+
170
+ **Стратегия:**
171
+ - Модели загружаются один раз при первом использовании
172
+ - Предзагрузка spaCy моделей при старте (опционально)
173
+ - BERT модель загружается лениво при первом запросе
174
+
175
+ ### Frontend
176
+
177
+ **Состояние:**
178
+ - `currentData` - последние результаты анализа
179
+ - DOM состояние для табов и форм
180
+
181
+ ## Производительность
182
+
183
+ ### Оптимизации
184
+
185
+ 1. **Кэширование моделей:**
186
+ - spaCy модели загружаются один раз
187
+ - BERT модель загружается один раз
188
+
189
+ 2. **Ленивая загрузка:**
190
+ - spaCy модели загружаются только для используемых языков
191
+ - BERT модель загружается при первом запросе
192
+
193
+ 3. **GPU ускорение:**
194
+ - Автоматическое использование CUDA для BERT
195
+ - Значительное ускорение на GPU
196
+
197
+ 4. **Ограничения:**
198
+ - N-граммы ограничены 150 элементами на тип
199
+ - Топ-5 chunks для BERT анализа
200
+
201
+ ### Ограничения
202
+
203
+ - Максимальная длина текста для spaCy: 2,000,000 символов
204
+ - Память: зависит от размера моделей и длины текстов
205
+ - Время обработки: зависит от длины текстов и наличия GPU
206
+
207
+ ## Масштабируемость
208
+
209
+ ### Текущие ограничения
210
+
211
+ - Однопоточная обработка запросов
212
+ - Модели загружаются в память
213
+ - Нет кэширования результатов
214
+
215
+ ### Возможные улучшения
216
+
217
+ 1. **Асинхронность:**
218
+ - Использование async/await для I/O операций
219
+ - Параллельная обработка конкурентов
220
+
221
+ 2. **Кэширование:**
222
+ - Redis для кэширования результатов
223
+ - Кэширование эмбеддингов
224
+
225
+ 3. **Микросервисы:**
226
+ - Отдельный сервис для BERT
227
+ - Отдельный сервис для spaCy
228
+
229
+ 4. **База данных:**
230
+ - Сохранение истории анализов
231
+ - Статистика использования
232
+
233
+ ## Безопасность
234
+
235
+ ### Текущее состояние
236
+
237
+ - Нет аутентификации
238
+ - Нет ограничений на размер запросов
239
+ - Нет валидации входных данных (кроме Pydantic)
240
+
241
+ ### Рекомендации
242
+
243
+ 1. **Валидация:**
244
+ - Ограничение размера текстов
245
+ - Санитизация входных данных
246
+
247
+ 2. **Аутентификация:**
248
+ - API ключи
249
+ - OAuth 2.0
250
+
251
+ 3. **Rate Limiting:**
252
+ - Ограничение количества запросов
253
+ - Защита от DDoS
254
+
255
+ ## Зависимости
256
+
257
+ ### Критические
258
+
259
+ - `fastapi` - веб-фреймворк
260
+ - `spacy` - NLP библиотека
261
+ - `sentence-transformers` - BERT модели
262
+ - `rank-bm25` - BM25 алгоритм
263
+ - `torch` - глубокое обучение
264
+
265
+ ### Вспомогательные
266
+
267
+ - `uvicorn` - ASGI сервер
268
+ - `pydantic` - валидация данных
269
+ - `jinja2` - шаблонизация
270
+ - `numpy` - численные вычисления
271
+
272
+ ## Расширяемость
273
+
274
+ ### Добавление нового языка
275
+
276
+ 1. Установить spaCy модель для языка
277
+ 2. Добавить в `MODEL_NAMES` в `logic.py`
278
+ 3. Добавить опцию в UI (`templates/index.html`)
279
+
280
+ ### Добавление новой модели BERT
281
+
282
+ 1. Изменить модель в `get_bert_model()`
283
+ 2. Убедиться в совместимости с `sentence-transformers`
284
+
285
+ ### Добавление нового типа анализа
286
+
287
+ 1. Создать функцию в `logic.py`
288
+ 2. Добавить вызов в `analyze_text()` в `main.py`
289
+ 3. Добавить поле в `AnalysisResponse`
290
+ 4. Обновить UI для отображения результатов
docs/DEVELOPMENT.md ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Руководство для разработчиков
2
+
3
+ ## Начало работы
4
+
5
+ ### Настройка окружения разработки
6
+
7
+ 1. Клонируйте репозиторий
8
+ 2. Создайте виртуальное окружение:
9
+ ```bash
10
+ python -m venv venv
11
+ venv\Scripts\activate # Windows
12
+ # или
13
+ source venv/bin/activate # Linux/Mac
14
+ ```
15
+
16
+ 3. Установите зависимости:
17
+ ```bash
18
+ pip install -r requirements.txt
19
+ ```
20
+
21
+ 4. Установите spaCy модели:
22
+ ```bash
23
+ python -m spacy download en_core_web_sm
24
+ python -m spacy download ru_core_news_sm
25
+ python -m spacy download de_core_news_sm
26
+ python -m spacy download es_core_news_sm
27
+ python -m spacy download it_core_news_sm
28
+ ```
29
+
30
+ ### Запуск в режиме разработки
31
+
32
+ ```bash
33
+ python main.py
34
+ ```
35
+
36
+ Или с автоматической перезагрузкой:
37
+ ```bash
38
+ uvicorn main:app --host 127.0.0.1 --port 8001 --reload
39
+ ```
40
+
41
+ ## Структура кода
42
+
43
+ ### Стиль кода
44
+
45
+ - Следуйте PEP 8 для Python
46
+ - Используйте type hints где возможно
47
+ - Документируйте функции docstrings
48
+ - Используйте понятные имена переменных
49
+
50
+ ### Организация кода
51
+
52
+ **main.py:**
53
+ - Только роутинг и обработка HTTP запросов
54
+ - Минимум бизнес-логики
55
+ - Делегирование в `logic.py`
56
+
57
+ **logic.py:**
58
+ - Вся бизнес-логика
59
+ - Разделение на модули (SPACY, ANALYTICS, BERT)
60
+ - Глобальные переменные для кэширования моделей
61
+
62
+ **models.py:**
63
+ - Только Pydantic модели
64
+ - Валидация данных
65
+ - Документация полей
66
+
67
+ ## Тестирование
68
+
69
+ ### Ручное тестирование
70
+
71
+ 1. Используйте Swagger UI: `http://127.0.0.1:8001/docs`
72
+ 2. Тестируйте через веб-интерфейс
73
+ 3. Проверяйте различные языки и размеры текстов
74
+
75
+ ### Примеры тестовых данных
76
+
77
+ **Русский язык:**
78
+ ```json
79
+ {
80
+ "target_text": "Это пример текста для анализа SEO оптимизации.",
81
+ "competitors": ["Конкурентный текст номер один с похожим содержанием.", "Второй конкурентный текст."],
82
+ "keywords": ["SEO анализ", "оптимизация текста"],
83
+ "language": "ru"
84
+ }
85
+ ```
86
+
87
+ **Английский язык:**
88
+ ```json
89
+ {
90
+ "target_text": "This is an example text for SEO analysis.",
91
+ "competitors": ["Competitor text number one.", "Second competitor text."],
92
+ "keywords": ["SEO analysis", "text optimization"],
93
+ "language": "en"
94
+ }
95
+ ```
96
+
97
+ ## Отладка
98
+
99
+ ### Логирование
100
+
101
+ Добавьте логирование в ключевых местах:
102
+
103
+ ```python
104
+ import logging
105
+
106
+ logging.basicConfig(level=logging.INFO)
107
+ logger = logging.getLogger(__name__)
108
+
109
+ logger.info("Loading model...")
110
+ ```
111
+
112
+ ### Проверка моделей
113
+
114
+ Проверьте загрузку моделей:
115
+ ```python
116
+ # В Python консоли
117
+ import logic
118
+ logic.load_models()
119
+ print(logic.LoadedModels.keys())
120
+ ```
121
+
122
+ ### Проверка BERT
123
+
124
+ ```python
125
+ import logic
126
+ model = logic.get_bert_model()
127
+ print(model.device) # Должно показать 'cuda' или 'cpu'
128
+ ```
129
+
130
+ ## Производительность
131
+
132
+ ### Профилирование
133
+
134
+ Используйте `cProfile` для профилирования:
135
+ ```python
136
+ import cProfile
137
+ import pstats
138
+
139
+ profiler = cProfile.Profile()
140
+ profiler.enable()
141
+
142
+ # Ваш код
143
+
144
+ profiler.disable()
145
+ stats = pstats.Stats(profiler)
146
+ stats.sort_stats('cumulative')
147
+ stats.print_stats(10)
148
+ ```
149
+
150
+ ### Оптимизация
151
+
152
+ 1. **Кэширование:**
153
+ - Модели уже кэшируются
154
+ - Рассмотрите кэширование результатов для одинаковых запросов
155
+
156
+ 2. **Параллелизация:**
157
+ - Обработка конкурентов может быть параллельной
158
+ - Используйте `asyncio` или `multiprocessing`
159
+
160
+ 3. **Батчинг:**
161
+ - BERT может обрабатывать несколько текстов одновременно
162
+ - Используйте батчи для эмбеддингов
163
+
164
+ ## Добавление новых функций
165
+
166
+ ### Добавление нового типа анализа
167
+
168
+ 1. Создайте функцию в `logic.py`:
169
+ ```python
170
+ def my_new_analysis(target_text: str, competitors: List[str], lang: str) -> Dict:
171
+ # Ваша логика
172
+ return {"result": "data"}
173
+ ```
174
+
175
+ 2. Добавьте вызов в `main.py`:
176
+ ```python
177
+ my_result = logic.my_new_analysis(
178
+ request.target_text,
179
+ request.competitors,
180
+ request.language
181
+ )
182
+ ```
183
+
184
+ 3. Добавьте поле в `AnalysisResponse`:
185
+ ```python
186
+ class AnalysisResponse(BaseModel):
187
+ # ... существующие поля
188
+ my_new_analysis: Dict
189
+ ```
190
+
191
+ 4. Обновите UI в `templates/index.html`
192
+
193
+ ### Добавление нового языка
194
+
195
+ 1. Установите spaCy модель:
196
+ ```bash
197
+ python -m spacy download <lang>_core_news_sm
198
+ ```
199
+
200
+ 2. Добавьте в `MODEL_NAMES`:
201
+ ```python
202
+ MODEL_NAMES = {
203
+ # ... существующие
204
+ "new_lang": "new_lang_core_news_sm"
205
+ }
206
+ ```
207
+
208
+ 3. Добавьте опцию в UI:
209
+ ```html
210
+ <option value="new_lang">🇺🇸 New Language</option>
211
+ ```
212
+
213
+ ## Работа с зависимостями
214
+
215
+ ### Обновление зависимостей
216
+
217
+ 1. Обновите версии в `requirements.txt`
218
+ 2. Установите:
219
+ ```bash
220
+ pip install -r requirements.txt --upgrade
221
+ ```
222
+
223
+ 3. Протестируйте приложение
224
+
225
+ ### Добавление новой зависимости
226
+
227
+ 1. Установите пакет:
228
+ ```bash
229
+ pip install new-package
230
+ ```
231
+
232
+ 2. Добавьте в `requirements.txt`:
233
+ ```bash
234
+ pip freeze > requirements.txt
235
+ ```
236
+
237
+ 3. Или добавьте вручную:
238
+ ```
239
+ new-package==1.0.0
240
+ ```
241
+
242
+ ## Git workflow
243
+
244
+ ### Коммиты
245
+
246
+ Используйте понятные сообщения коммитов:
247
+ ```
248
+ feat: добавлен анализ тональности
249
+ fix: исправлена ошибка в BM25 расчетах
250
+ docs: обновлена документация API
251
+ refactor: рефакторинг функции analyze_text
252
+ ```
253
+
254
+ ### Ветки
255
+
256
+ - `main` - стабильная версия
257
+ - `develop` - разработка
258
+ - `feature/название` - новая функция
259
+ - `fix/название` - исправление бага
260
+
261
+ ## Развертывание
262
+
263
+ ### Production настройки
264
+
265
+ 1. Отключите debug режим:
266
+ ```python
267
+ app = FastAPI(title="SEO AI Editor", debug=False)
268
+ ```
269
+
270
+ 2. Используйте production сервер:
271
+ ```bash
272
+ uvicorn main:app --host 0.0.0.0 --port 8001 --workers 4
273
+ ```
274
+
275
+ 3. Настройте переменные окружения:
276
+ ```python
277
+ import os
278
+ DEBUG = os.getenv("DEBUG", "False") == "True"
279
+ ```
280
+
281
+ ### Docker (опционально)
282
+
283
+ Создайте `Dockerfile`:
284
+ ```dockerfile
285
+ FROM python:3.10-slim
286
+
287
+ WORKDIR /app
288
+
289
+ COPY requirements.txt .
290
+ RUN pip install --no-cache-dir -r requirements.txt
291
+
292
+ COPY . .
293
+
294
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
295
+ ```
296
+
297
+ ## Известные проблемы
298
+
299
+ ### Память
300
+
301
+ - Большие тексты могут потреблять много памяти
302
+ - BERT модель занимает ~400MB RAM
303
+ - spaCy модели занимают ~50-100MB каждая
304
+
305
+ **Решение:** Ограничьте размер входных текстов или используйте потоковую обработку
306
+
307
+ ### Производительность
308
+
309
+ - Первый запрос медленнее (загрузка BERT)
310
+ - Длинные тексты обрабатываются дольше
311
+
312
+ **Решение:** Предзагрузка BERT модели, оптимизация алгоритмов
313
+
314
+ ### Языковые модели
315
+
316
+ - Некоторые языки могут иметь ограниченную поддержку
317
+ - Качество анализа зависит от качества моделей
318
+
319
+ **Решение:** Используйте более качественные модели или обучите свои
320
+
321
+ ## Полезные ресурсы
322
+
323
+ - [FastAPI документация](https://fastapi.tiangolo.com/)
324
+ - [spaCy документация](https://spacy.io/usage)
325
+ - [Sentence Transformers](https://www.sbert.net/)
326
+ - [BM25 алгоритм](https://en.wikipedia.org/wiki/Okapi_BM25)
327
+
328
+ ## Контакты и поддержка
329
+
330
+ Для вопросов и предложений:
331
+ - Создавайте issues в репозитории
332
+ - Предлагайте улучшения через pull requests
333
+ - Документируйте найденные баги
logic.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spacy
2
+ from collections import Counter
3
+ from typing import List, Dict
4
+ import numpy as np
5
+ from rank_bm25 import BM25Okapi
6
+
7
+ # Новые импорты для BERT
8
+ import torch
9
+ from sentence_transformers import SentenceTransformer, util
10
+
11
+ # --- Глобальные переменные ---
12
+ LoadedModels = {} # spaCy модели
13
+ BertModel = None # BERT модель (одна на все языки)
14
+
15
+ MODEL_NAMES = {
16
+ "en": "en_core_web_sm",
17
+ "ru": "ru_core_news_sm",
18
+ "de": "de_core_news_sm",
19
+ "es": "es_core_news_sm",
20
+ "it": "it_core_news_sm"
21
+ }
22
+
23
+ # --- SPACY (Лингвистика) ---
24
+
25
+ def load_model_if_missing(lang: str):
26
+ if lang in LoadedModels: return
27
+ model_name = MODEL_NAMES.get(lang)
28
+ if not model_name: return
29
+
30
+ print(f"⏳ Loading spaCy model for {lang}...")
31
+ try:
32
+ LoadedModels[lang] = spacy.load(model_name)
33
+ print(f"✅ Loaded spaCy: {lang}")
34
+ except Exception as e:
35
+ print(f"❌ Failed to load spaCy {lang}: {e}")
36
+
37
+ def load_models():
38
+ """
39
+ Функция для предзагрузки всех моделей при старте (вызывается из main.py).
40
+ """
41
+ print("🚀 Pre-loading all spaCy models...")
42
+ for lang in MODEL_NAMES.keys():
43
+ load_model_if_missing(lang)
44
+
45
+ def get_doc(text: str, lang: str):
46
+ load_model_if_missing(lang)
47
+ nlp = LoadedModels.get(lang)
48
+ if not nlp:
49
+ load_model_if_missing("en")
50
+ nlp = LoadedModels.get("en")
51
+
52
+ if not nlp: raise RuntimeError("No NLP models loaded.")
53
+ nlp.max_length = 2000000
54
+ return nlp(text.lower())
55
+
56
+ # --- НОВАЯ ФУНКЦИЯ ФИЛЬТРАЦИИ ---
57
+ def is_valid_token(t):
58
+ """
59
+ Проверяет, является ли токен полезным словом.
60
+ Исправленная версия: не удаляет слова из букв, даже если AI пометил их как символы.
61
+ """
62
+ # 1. Базовые проверки spaCy (Стоп-слова, пунктуация, пробелы)
63
+ if t.is_stop or t.is_punct or t.is_space:
64
+ return False
65
+
66
+ # 2. Числа (удаляем "18", "2023", "5")
67
+ if t.is_digit or t.like_num:
68
+ return False
69
+
70
+ # 3. СИМВОЛЫ (ИСПРАВЛЕНИЕ)
71
+ # Если spaCy говорит, что это символ (SYM), мы верим, ТОЛЬКО если это не буквы.
72
+ # Это спасет слова типа "cross", "apk", "bet", которые могут быть ложно помечены.
73
+ if t.pos_ == "SYM" and not t.text.isalpha():
74
+ return False
75
+
76
+ # 4. Дополнительная страховка (явный мусор)
77
+ garbage_chars = {'|', '+', '-', '—', '–', '>', '<', '=', '/', '\\', '★', '▶', '●', '•', '€', '$', '£'}
78
+ if t.text.strip() in garbage_chars:
79
+ return False
80
+
81
+ # 5. Длина: Удаляем одиночные буквы, которые не являются словами
82
+ # (опционально, но помогает чистить мусор типа "v", "s" если они не стоп-слова)
83
+ if len(t.text) == 1 and not t.text.isalpha():
84
+ return False
85
+
86
+ return True
87
+
88
+ def get_lemmas_flat(text: str, lang: str) -> List[str]:
89
+ """
90
+ Возвращает плоский список лемм для всего текста (нужен для BM25).
91
+ """
92
+ if not text: return []
93
+ doc = get_doc(text, lang)
94
+ # Используем нашу новую функцию фильтрации
95
+ return [t.lemma_ for t in doc if is_valid_token(t)]
96
+
97
+ def generate_ngrams_safe(text: str, lang: str, n: int) -> List[str]:
98
+ """
99
+ Генерирует n-граммы.
100
+ ИЗМЕНЕНИЕ: Оставляет стоп-слова внутри фраз, чтобы сохранить читаемость (gioco del pollo),
101
+ но фильтрует n-граммы, состоящие ТОЛЬКО из стоп-слов.
102
+ """
103
+ if not text: return []
104
+ doc = get_doc(text, lang)
105
+ all_ngrams = []
106
+
107
+ for sent in doc.sents:
108
+ # 1. Собираем токены предложения.
109
+ # Мы НЕ удаляем стоп-слова сразу, чтобы не рвать связность фразы.
110
+ # Но мы все еще чистим пунктуацию и явный мусор.
111
+
112
+ sent_tokens = []
113
+ for t in sent:
114
+ # Пропускаем пунктуацию, пробелы и символы
115
+ if t.is_punct or t.is_space or t.pos_ == "SYM":
116
+ continue
117
+ # Пропускаем явный мусор из нашего списка
118
+ garbage_chars = {'|', '+', '-', '—', '–', '>', '<', '=', '/', '\\', '★', '▶', '●', '•', '€', '$', '£'}
119
+ if t.text.strip() in garbage_chars:
120
+ continue
121
+
122
+ # Сохраняем токен: (Лемма, Является_ли_стоп_словом)
123
+ sent_tokens.append({
124
+ "lemma": t.lemma_,
125
+ "is_stop": t.is_stop
126
+ })
127
+
128
+ # 2. Генерируем N-граммы из очищенного списка
129
+ if len(sent_tokens) >= n:
130
+ # Скользящее окно
131
+ for i in range(len(sent_tokens) - n + 1):
132
+ window = sent_tokens[i : i+n]
133
+
134
+ # 3. ФИЛЬТР: Если ВСЕ слова в N-грамме - стоп-слова, пропускаем её.
135
+ # Пример: "e la" (биграмма из стоп-слов) -> мусор.
136
+ # Пример: "gioco del" (сущ + стоп) -> полезно.
137
+ if all(t["is_stop"] for t in window):
138
+ continue
139
+
140
+ # Склеиваем леммы
141
+ ngram_str = " ".join([t["lemma"] for t in window])
142
+ all_ngrams.append(ngram_str)
143
+
144
+ return all_ngrams
145
+
146
+ # --- ANALYTICS (N-grams & BM25) ---
147
+
148
+ def calculate_ngram_stats(target_text: str, competitor_texts: List[str], lang: str) -> Dict:
149
+ stats = {}
150
+
151
+ for n in range(1, 5):
152
+ key = {1: "unigrams", 2: "bigrams", 3: "trigrams", 4: "quadgrams"}[n]
153
+
154
+ target_ngrams = generate_ngrams_safe(target_text, lang, n)
155
+ target_counts = Counter(target_ngrams)
156
+
157
+ comp_counts_total = Counter()
158
+ for t in competitor_texts:
159
+ c_ngrams = generate_ngrams_safe(t, lang, n)
160
+ comp_counts_total.update(c_ngrams)
161
+
162
+ all_unique = set(target_counts.keys()) | set(comp_counts_total.keys())
163
+ ngram_data = []
164
+ num_competitors = max(len(competitor_texts), 1)
165
+
166
+ for ngram in all_unique:
167
+ cnt_target = target_counts.get(ngram, 0)
168
+ avg_comp = round(comp_counts_total.get(ngram, 0) / num_competitors, 1)
169
+
170
+ # Фильтр мусора: если слово встречается крайне редко везде (<0.5 в среднем), не показываем.
171
+ # Но если у нас оно есть (cnt_target > 0) - показываем всегда.
172
+ if cnt_target > 0 or avg_comp >= 0.5:
173
+ ngram_data.append({
174
+ "ngram": ngram,
175
+ "target_count": cnt_target,
176
+ "competitor_avg": avg_comp
177
+ })
178
+
179
+ # --- СОРТИРОВКА (ГЛАВНОЕ ИЗМЕНЕНИЕ) ---
180
+ # Сортируем по "Важности". Важность = Максимум из (частота у нас, частота у них).
181
+ # Это значит:
182
+ # 1. Если у нас слово 10 раз -> оно наверху.
183
+ # 2. Если у нас 0, а у них 10 раз -> оно ТОЖЕ наверху.
184
+ ngram_data.sort(key=lambda x: max(x["target_count"], x["competitor_avg"]), reverse=True)
185
+
186
+ stats[key] = ngram_data[:150]
187
+
188
+ return stats
189
+
190
+ def parse_keywords(raw_phrases: List[str], lang: str):
191
+ key_phrases = []
192
+ keywords = set()
193
+ for phrase in raw_phrases:
194
+ if not phrase.strip(): continue
195
+ lemmas = get_lemmas_flat(phrase, lang)
196
+ if lemmas:
197
+ key_phrases.append(phrase.strip()) # Для BERT храним исходную фразу, а не леммы!
198
+ for w in lemmas: keywords.add(w)
199
+ return list(key_phrases), list(keywords)
200
+
201
+ def calculate_bm25_recommendations(target_text: str, competitor_texts: List[str], raw_keywords: List[str], lang: str):
202
+ """
203
+ BM25 с полной декомпозицией фраз.
204
+ Если на входе "chicken road casino", мы анализируем:
205
+ 1. chicken, road, casino (Unigrams)
206
+ 2. chicken road, road casino (Bigrams)
207
+ 3. chicken road casino (Trigram)
208
+ """
209
+ if not target_text or not raw_keywords:
210
+ return []
211
+
212
+ recommendations = []
213
+
214
+ # 1. СБОР ВСЕХ ВОЗМОЖНЫХ КОМБИНАЦИЙ ИЗ КЛЮЧЕВЫХ ФРАЗ
215
+ analyzed_keys = []
216
+
217
+ # Используем set для отслеживания дубликатов на лету
218
+ seen_terms = set()
219
+
220
+ for phrase in raw_keywords:
221
+ if not phrase.strip(): continue
222
+
223
+ # Получаем токены (уже в нижнем регистре, без лемматизации, как мы исправили ранее)
224
+ tokens = get_lemmas_flat(phrase, lang)
225
+ if not tokens: continue
226
+
227
+ # Генерируем все под-н-граммы длиной от 1 до 3
228
+ # Если фраза длинная (5 слов), мы всё равно разобьем её на куски по 1, 2, 3 слова.
229
+ max_n = min(len(tokens), 3) # Анализируем не более чем триграммы
230
+
231
+ for n in range(1, max_n + 1):
232
+ # Скользящее окно по токенам фразы
233
+ for i in range(len(tokens) - n + 1):
234
+ window = tokens[i : i+n]
235
+ term = " ".join(window)
236
+
237
+ if term not in seen_terms:
238
+ analyzed_keys.append({
239
+ "n": n,
240
+ "term": term,
241
+ "original": phrase # Просто для справки, откуда пришло
242
+ })
243
+ seen_terms.add(term)
244
+
245
+ # 2. МНОГОУРОВНЕВЫЙ РАСЧЕТ BM25
246
+ for n in range(1, 4):
247
+ # Отбираем ключи текущей длины
248
+ current_n_keys = [k['term'] for k in analyzed_keys if k['n'] == n]
249
+ if not current_n_keys:
250
+ continue
251
+
252
+ # Строим корпус из N-грамм (Наш текст + Конкуренты)
253
+ target_ngrams = generate_ngrams_safe(target_text, lang, n)
254
+ comp_ngrams_list = [generate_ngrams_safe(t, lang, n) for t in competitor_texts]
255
+
256
+ corpus = [target_ngrams] + comp_ngrams_list
257
+
258
+ # Обучаем BM25
259
+ bm25 = BM25Okapi(corpus)
260
+
261
+ for term in current_n_keys:
262
+ scores = bm25.get_scores([term])
263
+ score_target = scores[0]
264
+ score_avg_comp = np.mean(scores[1:]) if len(scores) > 1 else 0
265
+
266
+ # --- Динамический порог ---
267
+ if n == 1:
268
+ threshold = 0.5
269
+ elif n == 2:
270
+ threshold = 0.25
271
+ else:
272
+ threshold = 0.15
273
+
274
+ action = "ok"
275
+ count_rec = 0
276
+
277
+ # Логика рекомендаций
278
+ if score_target < score_avg_comp - threshold:
279
+ action = "add"
280
+ factor = 0.5 if n == 1 else 0.4
281
+ count_rec = max(1, int((score_avg_comp - score_target) * factor))
282
+ elif score_target > score_avg_comp + threshold * 2:
283
+ action = "remove"
284
+ factor = 0.5
285
+ count_rec = max(1, int((score_target - score_avg_comp) * factor))
286
+
287
+ recommendations.append({
288
+ "word": term,
289
+ "type": f"{n}-gram",
290
+ "my_score": round(score_target, 2),
291
+ "avg_comp_score": round(score_avg_comp, 2),
292
+ "action": action,
293
+ "count": count_rec
294
+ })
295
+
296
+ # 3. СОРТИРОВКА
297
+ # 1. Сначала действия (ADD/REMOVE)
298
+ # 2. Потом по длине фразы (длинные интереснее: "gioco del pollo" выше чем "pollo")
299
+ # 3. Потом алфавит
300
+ recommendations.sort(key=lambda x: (
301
+ 0 if x["action"] != "ok" else 1,
302
+ -len(x["word"].split()),
303
+ x["word"]
304
+ ))
305
+
306
+ return recommendations
307
+
308
+ # --- BERT / VECTOR ANALYSIS ---
309
+
310
+ def get_bert_model():
311
+ """Загружает BERT на GPU, если он доступен"""
312
+ global BertModel
313
+ if BertModel is None:
314
+ device = "cuda" if torch.cuda.is_available() else "cpu"
315
+ print(f"🚀 Loading BERT model on {device}...")
316
+ # Используем легкую и мощную мультиязычную модель
317
+ BertModel = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2', device=device)
318
+ print("✅ BERT Loaded successfully.")
319
+ return BertModel
320
+
321
+ def perform_bert_analysis(target_text: str, competitor_texts: List[str], key_phrases: List[str], lang: str):
322
+ if not key_phrases:
323
+ return {"detailed": [], "global_scores": []}
324
+
325
+ model = get_bert_model()
326
+
327
+ # 1. Функция-помощник: Получить чанки и их эмбеддинги
328
+ def process_text(text):
329
+ if not text.strip(): return [], None
330
+ doc = get_doc(text, lang)
331
+ # Разбиваем на предложения > 10 символов
332
+ chunks = [sent.text.strip() for sent in doc.sents if len(sent.text.strip()) > 10]
333
+ if not chunks: return [], None
334
+ embeddings = model.encode(chunks, convert_to_tensor=True)
335
+ return chunks, embeddings
336
+
337
+ # 2. Обрабатываем Наш текст
338
+ target_chunks, target_emb = process_text(target_text)
339
+
340
+ # 3. Обрабатываем Конкурентов (сохраняем структуру)
341
+ competitors_data = []
342
+ for idx, comp_text in enumerate(competitor_texts):
343
+ chunks, emb = process_text(comp_text)
344
+ competitors_data.append({
345
+ "id": idx + 1,
346
+ "chunks": chunks,
347
+ "embeddings": emb
348
+ })
349
+
350
+ # Эмбеддинги ключей
351
+ keys_emb = model.encode(key_phrases, convert_to_tensor=True)
352
+
353
+ # --- РАСЧЕТ GLOBAL SCORE ---
354
+ # Global Score - это средний Max Score ��о всем ключевым словам.
355
+ # То есть, насколько хорошо текст покрывает ВСЕ ключи в среднем.
356
+
357
+ global_scores = []
358
+
359
+ # Считаем для нас
360
+ if target_emb is not None:
361
+ # Матрица [Key x Chunk]
362
+ sims = util.cos_sim(keys_emb, target_emb)
363
+ # Берем макс. сходство для каждого ключа (values), потом среднее по всем ключам
364
+ # torch.max возвращает (values, indices)
365
+ max_scores_per_key, _ = torch.max(sims, dim=1)
366
+ avg_relevance = torch.mean(max_scores_per_key).item()
367
+ global_scores.append({"name": "Мой текст", "score": round(avg_relevance, 3), "is_me": True})
368
+ else:
369
+ global_scores.append({"name": "Мой текст", "score": 0, "is_me": True})
370
+
371
+ # Считаем для конкурентов
372
+ for comp in competitors_data:
373
+ if comp["embeddings"] is not None:
374
+ sims = util.cos_sim(keys_emb, comp["embeddings"])
375
+ max_scores_per_key, _ = torch.max(sims, dim=1)
376
+ avg_relevance = torch.mean(max_scores_per_key).item()
377
+ global_scores.append({"name": f"Конкурент #{comp['id']}", "score": round(avg_relevance, 3), "is_me": False})
378
+ else:
379
+ global_scores.append({"name": f"Конкурент #{comp['id']}", "score": 0, "is_me": False})
380
+
381
+ # Сортируем глобальный рейтинг (победитель сверху)
382
+ global_scores.sort(key=lambda x: x["score"], reverse=True)
383
+
384
+ # --- ДЕТАЛЬНЫЙ АНАЛИЗ ПО ФРАЗАМ ---
385
+ detailed_results = []
386
+
387
+ for i, phrase in enumerate(key_phrases):
388
+ # 1. Анализ моего текста
389
+ my_top = []
390
+ my_max = 0
391
+ if target_emb is not None:
392
+ # Считаем снова локально или берем из матрицы (тут проще локально для чистоты кода)
393
+ # scores_target[i] уже посчитано выше в sims, но выше переменная sims переписывалась.
394
+ # Для надежности пересчитаем векторную близость для одной фразы (это мгновенно)
395
+ phrase_emb = keys_emb[i]
396
+ scores = util.cos_sim(phrase_emb, target_emb)[0] # вектор [chunks]
397
+
398
+ k = min(5, len(target_chunks))
399
+ vals, idxs = torch.topk(scores, k)
400
+ my_max = vals[0].item() if k > 0 else 0
401
+
402
+ for rank in range(k):
403
+ my_top.append({
404
+ "text": target_chunks[idxs[rank].item()],
405
+ "score": round(vals[rank].item(), 3)
406
+ })
407
+
408
+ # 2. Анализ конкурентов (Сборная солянка)
409
+ # Собираем все чанки всех конкурентов с их скорами и ID
410
+ all_comp_candidates = []
411
+
412
+ for comp in competitors_data:
413
+ if comp["embeddings"] is not None:
414
+ phrase_emb = keys_emb[i]
415
+ scores = util.cos_sim(phrase_emb, comp["embeddings"])[0]
416
+
417
+ # Берем топ-3 от каждого конкурента, чтобы добавить в общий пул
418
+ k = min(3, len(comp["chunks"]))
419
+ vals, idxs = torch.topk(scores, k)
420
+
421
+ for rank in range(k):
422
+ all_comp_candidates.append({
423
+ "text": comp["chunks"][idxs[rank].item()],
424
+ "score": vals[rank].item(),
425
+ "source": f"Конкурент #{comp['id']}" # <-- АТРИБУЦИЯ
426
+ })
427
+
428
+ # Сортируем общий пул конкурентов и берем ТОП-5 абсолютных лидеров
429
+ all_comp_candidates.sort(key=lambda x: x["score"], reverse=True)
430
+ comp_top_5 = all_comp_candidates[:5]
431
+
432
+ # Округляем скоры для вывода
433
+ for item in comp_top_5:
434
+ item["score"] = round(item["score"], 3)
435
+
436
+ comp_max = comp_top_5[0]["score"] if comp_top_5 else 0
437
+
438
+ # Статус
439
+ status = "ok"
440
+ rec = "Тема раскрыта хорошо."
441
+ if my_max < 0.5:
442
+ status = "bad"
443
+ rec = "Тема не раскрыта."
444
+ elif comp_max > my_max + 0.1:
445
+ status = "warning"
446
+ rec = "Конкуренты раскрыли тему заметно лучше."
447
+ elif my_max >= 0.7:
448
+ status = "good"
449
+ rec = "Отлично."
450
+
451
+ detailed_results.append({
452
+ "phrase": phrase,
453
+ "my_max_score": round(my_max, 2),
454
+ "comp_max_score": round(comp_max, 2),
455
+ "status": status,
456
+ "recommendation": rec,
457
+ "my_top_chunks": my_top,
458
+ "comp_top_chunks": comp_top_5
459
+ })
460
+
461
+ return {
462
+ "global_scores": global_scores,
463
+ "detailed": detailed_results
464
+ }
main.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.responses import HTMLResponse
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.templating import Jinja2Templates
7
+ import uvicorn
8
+ import torch
9
+
10
+ from models import AnalysisRequest, AnalysisResponse
11
+ import logic
12
+
13
+ app = FastAPI(title="SEO AI Editor MVP")
14
+
15
+ # Подключаем папку с шаблонами
16
+ templates = Jinja2Templates(directory="templates")
17
+
18
+ @app.on_event("startup")
19
+ async def startup_event():
20
+ device = "cuda" if torch.cuda.is_available() else "cpu"
21
+ print(f"🚀 Application starting. ML Device: {device}")
22
+ logic.load_models() # spaCy preload (optional)
23
+
24
+ # --- НОВЫЙ РОУТ ДЛЯ ГЛАВНОЙ СТРАНИЦЫ ---
25
+ @app.get("/", response_class=HTMLResponse)
26
+ async def read_root(request: Request):
27
+ # Рендерим файл index.html
28
+ return templates.TemplateResponse("index.html", {"request": request})
29
+
30
+ @app.post("/analyze", response_model=AnalysisResponse)
31
+ async def analyze_text(request: AnalysisRequest):
32
+ # Логика та же самая, что и была
33
+ ngram_stats_result = logic.calculate_ngram_stats(
34
+ request.target_text,
35
+ request.competitors,
36
+ request.language
37
+ )
38
+
39
+ key_phrases, key_words_unigrams = logic.parse_keywords(request.keywords, request.language)
40
+
41
+ bm25_recs = logic.calculate_bm25_recommendations(
42
+ request.target_text,
43
+ request.competitors,
44
+ request.keywords, # <-- ИЗМЕНЕНИЕ ЗДЕСЬ (было key_words_unigrams)
45
+ request.language
46
+ )
47
+
48
+ bert_results = logic.perform_bert_analysis(
49
+ request.target_text,
50
+ request.competitors, # <-- ДОБАВИЛИ ЭТОТ АРГУМЕНТ
51
+ key_phrases,
52
+ request.language
53
+ )
54
+
55
+ return AnalysisResponse(
56
+ ngram_stats=ngram_stats_result,
57
+ bm25_recommendations=bm25_recs,
58
+ bert_analysis=bert_results
59
+ )
60
+
61
+ if __name__ == "__main__":
62
+ uvicorn.run("main:app", host="127.0.0.1", port=8001, reload=True)
models.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+ from typing import List, Dict, Optional, Any
4
+
5
+
6
+ class CompetitorText(BaseModel):
7
+ id: int
8
+ text: str
9
+
10
+ class AnalysisRequest(BaseModel):
11
+ target_text: str # Текст пользователя
12
+ competitors: List[str] # Список текстов конкурентов
13
+ keywords: List[str] # Список ключевых фраз (сырых)
14
+ language: str = "en" # en, ru, de, es, it
15
+
16
+ class AnalysisResponse(BaseModel):
17
+ ngram_stats: dict # Статистика униграм/биграм
18
+ bm25_recommendations: List[dict] # Рекомендации "добавить/убрать"
19
+ bert_analysis: Dict[str, Any] # Векторный анализ
ps.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ $env:temp = "D:\seo_ai_editor\pip_temp"
2
+ $env:tmp = "D:\seo_ai_editor\pip_temp"
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ numpy
5
+ scikit-learn
6
+ rank-bm25
7
+ sentence-transformers
8
+ spacy
9
+ python-multipart
10
+ jinja2
templates/index.html ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SEO AI Editor (GPU Powered)</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body { background-color: #f8f9fa; }
10
+ .editor-box { min-height: 300px; font-family: 'Georgia', serif; font-size: 1.1rem; }
11
+ .stat-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; margin-bottom: 20px; }
12
+ .loading-overlay {
13
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
14
+ background: rgba(255,255,255,0.8); z-index: 9999; text-align: center; padding-top: 20%;
15
+ }
16
+ /* Стили для скролла в таблицах */
17
+ .scrollable-table { max-height: 500px; overflow-y: auto; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+
22
+ <!-- Лоадер -->
23
+ <div id="loader" class="loading-overlay">
24
+ <div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status"></div>
25
+ <h3 class="mt-3">AI анализирует текст...</h3>
26
+ <p class="text-muted">Первый запуск BERT может занять пару секунд</p>
27
+ </div>
28
+
29
+ <nav class="navbar navbar-dark bg-dark mb-4">
30
+ <div class="container-fluid">
31
+ <span class="navbar-brand mb-0 h1">🚀 SEO AI Editor <small class="text-secondary" style="font-size: 0.6em;">v1.2 BERT+Comparisons</small></span>
32
+ </div>
33
+ </nav>
34
+
35
+ <div class="container-fluid">
36
+ <div class="row">
37
+ <!-- ЛЕВАЯ КОЛОНКА: ВВОД ДАННЫХ -->
38
+ <div class="col-md-5">
39
+ <div class="stat-card">
40
+ <div class="mb-3">
41
+ <label class="form-label fw-bold">Язык анализа</label>
42
+ <select class="form-select" id="languageSelect">
43
+ <option value="ru">🇷🇺 Русский</option>
44
+ <option value="en">🇺🇸 English</option>
45
+ <option value="de">🇩🇪 Deutsch</option>
46
+ <option value="it">🇮🇹 Italiano</option>
47
+ <option value="es">🇪🇸 Español</option>
48
+ </select>
49
+ </div>
50
+
51
+ <div class="mb-3">
52
+ <label class="form-label fw-bold">Ваш текст (Target)</label>
53
+ <textarea class="form-control editor-box" id="targetText" placeholder="Пишите текст здесь..."></textarea>
54
+ </div>
55
+
56
+ <div class="mb-3">
57
+ <label class="form-label fw-bold">Ключевые фразы</label>
58
+ <small class="text-muted d-block mb-1">Каждая фраза с новой строки</small>
59
+ <textarea class="form-control" id="keywordsInput" rows="5" placeholder="купить слона&#10;лучшие цены"></textarea>
60
+ </div>
61
+
62
+ <div class="mb-3">
63
+ <label class="form-label fw-bold">Тексты конкурентов</label>
64
+ <div id="competitorsList">
65
+ <!-- Поля добавляются сюда -->
66
+ <textarea class="form-control mb-2" rows="3" placeholder="Текст конкурента 1..."></textarea>
67
+ </div>
68
+ <button class="btn btn-sm btn-outline-secondary mt-1" onclick="addCompetitorField()">+ Добавить конкурента</button>
69
+ </div>
70
+
71
+ <div class="d-grid gap-2">
72
+ <button class="btn btn-primary btn-lg" onclick="runAnalysis()">⚡ Анализировать (GPU)</button>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- ПРАВАЯ КОЛОНКА: РЕЗУЛЬТАТЫ -->
78
+ <div class="col-md-7">
79
+
80
+ <!-- Табы -->
81
+ <ul class="nav nav-tabs mb-3" id="resultsTab" role="tablist">
82
+ <li class="nav-item">
83
+ <button class="nav-link active" id="bert-tab" data-bs-toggle="tab" data-bs-target="#bert" type="button">🧠 BERT Семантика</button>
84
+ </li>
85
+ <li class="nav-item">
86
+ <button class="nav-link" id="bm25-tab" data-bs-toggle="tab" data-bs-target="#bm25" type="button">📊 BM25 Баланс</button>
87
+ </li>
88
+ <li class="nav-item">
89
+ <button class="nav-link" id="ngrams-tab" data-bs-toggle="tab" data-bs-target="#ngrams" type="button">🔠 N-граммы</button>
90
+ </li>
91
+ </ul>
92
+
93
+ <div class="tab-content" id="resultsContent">
94
+
95
+ <!-- BERT TAB (НОВЫЙ) -->
96
+ <div class="tab-pane fade show active" id="bert" role="tabpanel">
97
+ <div class="stat-card">
98
+ <h5 class="card-title">Семантический анализ (BERT)</h5>
99
+ <p class="text-muted small">Сравнение глубины раскрытия темы у вас и у конкурентов.</p>
100
+ <div id="bertResultsContainer">
101
+ <div class="text-center text-muted py-5">Нажмите "Анализировать", чтобы увидеть результаты.</div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- BM25 TAB -->
107
+ <div class="tab-pane fade" id="bm25" role="tabpanel">
108
+ <div class="stat-card">
109
+ <h5 class="card-title">Частотный баланс (BM25)</h5>
110
+ <p class="text-muted small">Рекомендации по добавлению/удалению слов.</p>
111
+ <div class="scrollable-table">
112
+ <table class="table table-hover">
113
+ <thead>
114
+ <tr>
115
+ <th>Слово</th>
116
+ <th>Действие</th>
117
+ <th>Кол-во</th>
118
+ <th>Мой Score</th>
119
+ <th>Avg Comp</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody id="bm25TableBody">
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ <div id="bm25EmptyMsg" class="text-center text-muted py-3">Нет критических рекомендаций.</div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- N-GRAMS TAB -->
131
+ <div class="tab-pane fade" id="ngrams" role="tabpanel">
132
+ <div class="stat-card">
133
+ <h5 class="card-title">Статистика слов</h5>
134
+ <div class="mb-3">
135
+ <div class="btn-group" role="group">
136
+ <button type="button" class="btn btn-outline-primary active" onclick="showNgramTable('unigrams')">1 слово</button>
137
+ <button type="button" class="btn btn-outline-primary" onclick="showNgramTable('bigrams')">2 слова</button>
138
+ <button type="button" class="btn btn-outline-primary" onclick="showNgramTable('trigrams')">3 слова</button>
139
+ </div>
140
+ </div>
141
+ <div class="scrollable-table">
142
+ <table class="table table-sm">
143
+ <thead>
144
+ <tr>
145
+ <th>Фраза</th>
146
+ <th>У меня</th>
147
+ <th>У конкурентов (avg)</th>
148
+ </tr>
149
+ </thead>
150
+ <tbody id="ngramTableBody"></tbody>
151
+ </table>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
162
+ <script>
163
+ let currentData = null;
164
+
165
+ // --- ФУНКЦИИ ИНТЕРФЕЙСА ---
166
+
167
+ function addCompetitorField() {
168
+ const div = document.createElement('div');
169
+ div.innerHTML = '<textarea class="form-control mb-2 competitor-input" rows="3" placeholder="Ещё конкурент..."></textarea>';
170
+ document.getElementById('competitorsList').appendChild(div);
171
+ }
172
+
173
+ async function runAnalysis() {
174
+ // Сбор данных
175
+ const targetText = document.getElementById('targetText').value;
176
+ const lang = document.getElementById('languageSelect').value;
177
+ const keywordsRaw = document.getElementById('keywordsInput').value.split('\n').filter(k => k.trim() !== '');
178
+
179
+ // Сбор конкурентов
180
+ const compInputs = document.querySelectorAll('#competitorsList textarea');
181
+ const competitors = [];
182
+ compInputs.forEach(input => {
183
+ if(input.value.trim() !== '') competitors.push(input.value);
184
+ });
185
+
186
+ if(!targetText) { alert("Введите ваш текст!"); return; }
187
+
188
+ // UI Loading
189
+ document.getElementById('loader').style.display = 'block';
190
+
191
+ const payload = {
192
+ target_text: targetText,
193
+ competitors: competitors,
194
+ keywords: keywordsRaw,
195
+ language: lang
196
+ };
197
+
198
+ try {
199
+ const response = await fetch('/analyze', {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify(payload)
203
+ });
204
+
205
+ if (!response.ok) throw new Error("Ошибка сервера: " + response.statusText);
206
+
207
+ const data = await response.json();
208
+ currentData = data;
209
+ renderResults(data);
210
+
211
+ } catch (error) {
212
+ alert("Ошибка: " + error.message);
213
+ console.error(error);
214
+ } finally {
215
+ document.getElementById('loader').style.display = 'none';
216
+ }
217
+ }
218
+
219
+ function renderResults(data) {
220
+ // 1. BERT Render (ИСПРАВЛЕННЫЙ ПОД НОВУЮ СТРУКТУРУ)
221
+ const bertContainer = document.getElementById('bertResultsContainer');
222
+ bertContainer.innerHTML = '';
223
+
224
+ // Получаем объект данных. Теперь это объект, а не массив!
225
+ const bertData = data.bert_analysis;
226
+
227
+ // Проверяем, есть ли поле detailed (список фраз)
228
+ if (!bertData || !bertData.detailed || bertData.detailed.length === 0) {
229
+ bertContainer.innerHTML = '<div class="alert alert-warning">Добавьте ключевые фразы для анализа.</div>';
230
+ } else {
231
+
232
+ // А. Рендерим ГЛОБАЛЬНЫЙ СЧЕТ (Global Score)
233
+ if (bertData.global_scores && bertData.global_scores.length > 0) {
234
+ let globalHtml = '<div class="card mb-4 border-primary"><div class="card-body">';
235
+ globalHtml += '<h6 class="card-title text-primary fw-bold mb-3">🏆 Общий рейтинг релевантности (Global Score)</h6>';
236
+
237
+ bertData.global_scores.forEach(gs => {
238
+ const scorePct = Math.round(gs.score * 100);
239
+ const isMe = gs.is_me;
240
+ const barColor = isMe ? 'bg-primary' : 'bg-secondary';
241
+ const rowBg = isMe ? 'bg-light border-start border-primary border-3' : '';
242
+ const nameLabel = isMe ? `<strong>${gs.name} (Вы)</strong>` : gs.name;
243
+
244
+ globalHtml += `
245
+ <div class="d-flex align-items-center mb-2 p-2 rounded ${rowBg}">
246
+ <div style="width: 150px;">${nameLabel}</div>
247
+ <div class="flex-grow-1 mx-3">
248
+ <div class="progress" style="height: 20px;">
249
+ <div class="progress-bar ${barColor}" role="progressbar" style="width: ${scorePct}%">${scorePct}%</div>
250
+ </div>
251
+ </div>
252
+ <div class="fw-bold">${gs.score}</div>
253
+ </div>`;
254
+ });
255
+ globalHtml += '</div></div>';
256
+ bertContainer.insertAdjacentHTML('beforeend', globalHtml);
257
+ }
258
+
259
+ // Б. Рендерим ДЕТАЛИЗАЦИЮ (Аккордеоны)
260
+ // Итерируемся именно по .detailed, так как это массив
261
+ bertData.detailed.forEach((item, index) => {
262
+ let badgeClass = 'bg-secondary';
263
+ if(item.status === 'good') badgeClass = 'bg-success';
264
+ if(item.status === 'warning') badgeClass = 'bg-warning text-dark';
265
+ if(item.status === 'bad') badgeClass = 'bg-danger';
266
+
267
+ const collapseId = `collapseBert${index}`;
268
+
269
+ // Мои чанки
270
+ const myChunksHtml = item.my_top_chunks.map(c =>
271
+ `<li class="list-group-item d-flex justify-content-between align-items-start border-0 border-bottom">
272
+ <div class="small me-2">"${c.text}"</div>
273
+ <span class="badge bg-primary rounded-pill opacity-75">${c.score}</span>
274
+ </li>`
275
+ ).join('');
276
+
277
+ // Чанки конкурентов (С АТРИБУЦИЕЙ ИСТОЧНИКА)
278
+ const compChunksHtml = (item.comp_top_chunks && item.comp_top_chunks.length > 0)
279
+ ? item.comp_top_chunks.map(c =>
280
+ `<li class="list-group-item d-flex justify-content-between align-items-start border-0 border-bottom list-group-item-light">
281
+ <div class="me-2">
282
+ <span class="badge bg-secondary mb-1" style="font-size: 0.7em;">${c.source}</span>
283
+ <div class="small text-muted">"${c.text}"</div>
284
+ </div>
285
+ <span class="badge bg-dark rounded-pill opacity-50">${c.score}</span>
286
+ </li>`
287
+ ).join('')
288
+ : '<li class="list-group-item text-muted small border-0">Нет данных</li>';
289
+
290
+ const html = `
291
+ <div class="card mb-3 border">
292
+ <div class="card-header bg-white d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#${collapseId}">
293
+ <div>
294
+ <div class="fw-bold text-dark">${item.phrase}</div>
295
+ <div class="text-muted small">
296
+ My: <b>${item.my_max_score}</b> vs Best Comp: <b>${item.comp_max_score}</b>
297
+ </div>
298
+ </div>
299
+ <span class="badge ${badgeClass}">${item.status.toUpperCase()}</span>
300
+ </div>
301
+
302
+ <div id="${collapseId}" class="collapse">
303
+ <div class="card-body bg-light">
304
+ <p class="small mb-3"><strong>Совет:</strong> ${item.recommendation}</p>
305
+ <div class="row">
306
+ <div class="col-md-6">
307
+ <h6 class="small fw-bold text-primary">Мой текст</h6>
308
+ <ul class="list-group shadow-sm mb-3">${myChunksHtml || '<li class="list-group-item small">Нет вхождений</li>'}</ul>
309
+ </div>
310
+ <div class="col-md-6 border-start">
311
+ <h6 class="small fw-bold text-secondary">Лучшее у конкурентов</h6>
312
+ <ul class="list-group shadow-sm">${compChunksHtml}</ul>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>`;
318
+ bertContainer.insertAdjacentHTML('beforeend', html);
319
+ });
320
+ }
321
+
322
+ // 2. BM25 Render (ОБНОВЛЕННЫЙ v2 - Полный список)
323
+ const bm25Body = document.getElementById('bm25TableBody');
324
+ bm25Body.innerHTML = '';
325
+ const bm25Msg = document.getElementById('bm25EmptyMsg');
326
+
327
+ // Теперь мы ожидаем, что список не пуст, если были введены ключи
328
+ if (data.bm25_recommendations && data.bm25_recommendations.length > 0) {
329
+ bm25Msg.style.display = 'none';
330
+
331
+ data.bm25_recommendations.forEach(item => {
332
+ let colorClass = '';
333
+ let actionText = '';
334
+ let countText = '';
335
+ let rowBg = '';
336
+
337
+ // Определяем стили в зависимости от действия
338
+ if (item.action === 'add') {
339
+ colorClass = 'text-success';
340
+ actionText = 'ДОБАВИТЬ';
341
+ countText = `+${item.count}`;
342
+ rowBg = 'table-success'; // Легкая зеленая подсветка всей строки (Bootstrap класс)
343
+ // Но лучше не красить всю строку, чтобы не рябило, покрасим только текст действия
344
+ rowBg = '';
345
+ } else if (item.action === 'remove') {
346
+ colorClass = 'text-danger';
347
+ actionText = 'УБРАТЬ';
348
+ countText = `-${item.count}`;
349
+ } else {
350
+ colorClass = 'text-muted'; // Серый цвет
351
+ actionText = 'НОРМА'; // Или OK
352
+ countText = '<span class="text-muted">-</span>';
353
+ }
354
+
355
+ // Жирный шрифт для важных действий
356
+ const weight = item.action === 'ok' ? 'fw-normal' : 'fw-bold';
357
+
358
+ const row = `
359
+ <tr>
360
+ <td class="fw-bold text-dark">${item.word}</td>
361
+ <td class="${colorClass} ${weight}">${actionText}</td>
362
+ <td class="${colorClass} ${weight}">${countText}</td>
363
+ <td>${item.my_score}</td>
364
+ <td>${item.avg_comp_score}</td>
365
+ </tr>`;
366
+ bm25Body.insertAdjacentHTML('beforeend', row);
367
+ });
368
+ } else {
369
+ // Если список пуст (например, не ввели ключевые слова)
370
+ bm25Msg.style.display = 'block';
371
+ bm25Msg.textContent = "Введите ключевые фразы для расчета BM25.";
372
+ }
373
+
374
+ // 3. N-grams (ОСТАВЛЯЕМ КАК ЕСТЬ)
375
+ showNgramTable('unigrams');
376
+ }
377
+
378
+ function showNgramTable(type) {
379
+ if(!currentData) return;
380
+
381
+ // Логика переключения кнопок (visual state)
382
+ document.querySelectorAll('#ngrams .btn').forEach(b => {
383
+ // Простая проверка по тексту кнопки ("1 слово", "2 слова"...)
384
+ if(type === 'unigrams' && b.innerText.includes('1')) b.classList.add('active');
385
+ else if(type === 'bigrams' && b.innerText.includes('2')) b.classList.add('active');
386
+ else if(type === 'trigrams' && b.innerText.includes('3')) b.classList.add('active');
387
+ else b.classList.remove('active');
388
+ });
389
+
390
+ const tbody = document.getElementById('ngramTableBody');
391
+ tbody.innerHTML = '';
392
+
393
+ const list = currentData.ngram_stats[type];
394
+
395
+ if(list && list.length > 0) {
396
+ list.forEach(item => {
397
+ let rowClass = "";
398
+ let countClass = "";
399
+ let icon = "";
400
+
401
+ // Логика подсветки
402
+ if (item.target_count === 0 && item.competitor_avg > 0) {
403
+ // У НИХ ЕСТЬ, У НАС НЕТ -> ВАЖНО!
404
+ rowClass = "table-warning"; // Желтоватый фон (Bootstrap)
405
+ countClass = "text-danger fw-bold";
406
+ icon = "⚠️"; // Предупреждение
407
+ } else if (item.target_count > 0 && item.competitor_avg === 0) {
408
+ // У нас есть, у них нет (наша уникальность)
409
+ // Можно выделить зеленым, но не обязательно, это не проблема.
410
+ countClass = "text-success";
411
+ }
412
+
413
+ const row = `
414
+ <tr class="${rowClass}">
415
+ <td>${item.ngram} ${icon}</td>
416
+ <td class="fw-bold ${countClass}">${item.target_count}</td>
417
+ <td>${item.competitor_avg}</td>
418
+ </tr>`;
419
+ tbody.insertAdjacentHTML('beforeend', row);
420
+ });
421
+ } else {
422
+ tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">Нет данных</td></tr>';
423
+ }
424
+ }
425
+ </script>
426
+ </body>
427
+ </html>