Spaces:
Sleeping
Sleeping
SEO AI Tool
#2
by coingimp - opened
- app.py +1 -84
- docs/FULL_FUNCTIONAL_DOCUMENTATION.md +9 -55
- docs/HF_SES_AND_UI.md +0 -28
- docs/TEXT_OPTIMIZER_PRINCIPLES.md +0 -99
- models.py +1 -34
- optimizer.py +0 -0
- static/js/app.js +0 -0
- templates/index.html +0 -0
app.py
CHANGED
|
@@ -1,13 +1,7 @@
|
|
| 1 |
# app.py - Hugging Face Spaces entry point
|
| 2 |
|
| 3 |
-
import json
|
| 4 |
-
import threading
|
| 5 |
-
import uuid
|
| 6 |
-
from queue import Empty, Queue
|
| 7 |
-
|
| 8 |
from fastapi import FastAPI, Request
|
| 9 |
-
from fastapi.responses import HTMLResponse
|
| 10 |
-
from fastapi.staticfiles import StaticFiles
|
| 11 |
from fastapi.templating import Jinja2Templates
|
| 12 |
import uvicorn
|
| 13 |
import torch
|
|
@@ -25,7 +19,6 @@ from models import (
|
|
| 25 |
UserAgentsResponse,
|
| 26 |
OptimizerRequest,
|
| 27 |
OptimizerResponse,
|
| 28 |
-
OptimizerCancelRequest,
|
| 29 |
)
|
| 30 |
import logic
|
| 31 |
import nlp_processor
|
|
@@ -38,13 +31,6 @@ import optimizer
|
|
| 38 |
|
| 39 |
app = FastAPI(title="SEO AI Editor MVP")
|
| 40 |
|
| 41 |
-
_static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
| 42 |
-
if os.path.isdir(_static_dir):
|
| 43 |
-
app.mount("/static", StaticFiles(directory=_static_dir), name="static")
|
| 44 |
-
|
| 45 |
-
_OPTIMIZER_JOBS_LOCK = threading.Lock()
|
| 46 |
-
_OPTIMIZER_CANCEL_EVENTS: dict = {}
|
| 47 |
-
|
| 48 |
# Подключаем папку с шаблонами
|
| 49 |
templates = Jinja2Templates(directory="templates")
|
| 50 |
|
|
@@ -272,75 +258,6 @@ async def run_optimizer(request: OptimizerRequest):
|
|
| 272 |
except Exception as e:
|
| 273 |
return OptimizerResponse(ok=False, error=str(e))
|
| 274 |
|
| 275 |
-
|
| 276 |
-
@app.post("/api/v1/optimizer/cancel")
|
| 277 |
-
async def optimizer_cancel(body: OptimizerCancelRequest):
|
| 278 |
-
with _OPTIMIZER_JOBS_LOCK:
|
| 279 |
-
ev = _OPTIMIZER_CANCEL_EVENTS.get(body.job_id)
|
| 280 |
-
if ev is not None:
|
| 281 |
-
ev.set()
|
| 282 |
-
return {"ok": True}
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
@app.post("/api/v1/optimizer/run-stream")
|
| 286 |
-
async def run_optimizer_stream(request: OptimizerRequest):
|
| 287 |
-
"""SSE: события прогресса + финальный JSON. Клиент ведёт локальный лог, без глобального лоадера."""
|
| 288 |
-
job_id = str(uuid.uuid4())
|
| 289 |
-
cancel_ev = threading.Event()
|
| 290 |
-
payload = request.model_dump()
|
| 291 |
-
q: Queue = Queue()
|
| 292 |
-
|
| 293 |
-
with _OPTIMIZER_JOBS_LOCK:
|
| 294 |
-
_OPTIMIZER_CANCEL_EVENTS[job_id] = cancel_ev
|
| 295 |
-
|
| 296 |
-
def worker():
|
| 297 |
-
try:
|
| 298 |
-
def progress_cb(data):
|
| 299 |
-
q.put(("progress", data))
|
| 300 |
-
|
| 301 |
-
result = optimizer.optimize_text(
|
| 302 |
-
payload,
|
| 303 |
-
progress_callback=progress_cb,
|
| 304 |
-
cancel_event=cancel_ev,
|
| 305 |
-
)
|
| 306 |
-
q.put(("done", result))
|
| 307 |
-
except Exception as e:
|
| 308 |
-
q.put(("error", str(e)))
|
| 309 |
-
|
| 310 |
-
threading.Thread(target=worker, daemon=True).start()
|
| 311 |
-
|
| 312 |
-
def gen():
|
| 313 |
-
try:
|
| 314 |
-
yield f"data: {json.dumps({'event': 'job', 'job_id': job_id})}\n\n"
|
| 315 |
-
while True:
|
| 316 |
-
try:
|
| 317 |
-
kind, data = q.get(timeout=0.3)
|
| 318 |
-
except Empty:
|
| 319 |
-
yield ": ping\n\n"
|
| 320 |
-
continue
|
| 321 |
-
if kind == "progress":
|
| 322 |
-
yield f"data: {json.dumps(data)}\n\n"
|
| 323 |
-
elif kind == "done":
|
| 324 |
-
yield f"data: {json.dumps({'event': 'complete', 'result': data})}\n\n"
|
| 325 |
-
break
|
| 326 |
-
elif kind == "error":
|
| 327 |
-
yield f"data: {json.dumps({'event': 'error', 'error': data})}\n\n"
|
| 328 |
-
break
|
| 329 |
-
finally:
|
| 330 |
-
with _OPTIMIZER_JOBS_LOCK:
|
| 331 |
-
_OPTIMIZER_CANCEL_EVENTS.pop(job_id, None)
|
| 332 |
-
|
| 333 |
-
return StreamingResponse(
|
| 334 |
-
gen(),
|
| 335 |
-
media_type="text/event-stream",
|
| 336 |
-
headers={
|
| 337 |
-
"Cache-Control": "no-cache",
|
| 338 |
-
"Connection": "keep-alive",
|
| 339 |
-
"X-Accel-Buffering": "no",
|
| 340 |
-
},
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
|
| 344 |
# Hugging Face Spaces использует порт 7860
|
| 345 |
if __name__ == "__main__":
|
| 346 |
port = int(os.environ.get("PORT", 7860))
|
|
|
|
| 1 |
# app.py - Hugging Face Spaces entry point
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from fastapi import FastAPI, Request
|
| 4 |
+
from fastapi.responses import HTMLResponse
|
|
|
|
| 5 |
from fastapi.templating import Jinja2Templates
|
| 6 |
import uvicorn
|
| 7 |
import torch
|
|
|
|
| 19 |
UserAgentsResponse,
|
| 20 |
OptimizerRequest,
|
| 21 |
OptimizerResponse,
|
|
|
|
| 22 |
)
|
| 23 |
import logic
|
| 24 |
import nlp_processor
|
|
|
|
| 31 |
|
| 32 |
app = FastAPI(title="SEO AI Editor MVP")
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# Подключаем папку с шаблонами
|
| 35 |
templates = Jinja2Templates(directory="templates")
|
| 36 |
|
|
|
|
| 258 |
except Exception as e:
|
| 259 |
return OptimizerResponse(ok=False, error=str(e))
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
# Hugging Face Spaces использует порт 7860
|
| 262 |
if __name__ == "__main__":
|
| 263 |
port = int(os.environ.get("PORT", 7860))
|
docs/FULL_FUNCTIONAL_DOCUMENTATION.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
| 24 |
- смысловой поиск по словам и фразам
|
| 25 |
- сравнение с конкурентами (включая таблицу мощных терминов)
|
| 26 |
|
| 27 |
-
3. **LLM Optimizer** (`POST /api/v1/optimizer/run`
|
| 28 |
- итеративная локальная оптимизация текста
|
| 29 |
- многокритериальный скоринг с защитой от деградации
|
| 30 |
- каскад уровней правок (от минимальных к более широким)
|
|
@@ -44,9 +44,7 @@
|
|
| 44 |
- `search.py` — смысловой поиск в графе (фразы + слова).
|
| 45 |
- `url_fetcher.py` — извлечение текста/title из URL с выбором user-agent.
|
| 46 |
- `optimizer.py` — LLM-оптимизация с обратной связью от метрик.
|
| 47 |
-
- `
|
| 48 |
-
- `templates/index.html` — разметка UI.
|
| 49 |
-
- `static/js/app.js` — вся клиентская логика (подключается как `/static/js/app.js`; без гигантского inline-скрипта — см. `docs/HF_SES_AND_UI.md`).
|
| 50 |
|
| 51 |
---
|
| 52 |
|
|
@@ -166,32 +164,16 @@
|
|
| 166 |
### Вход (`OptimizerRequest`)
|
| 167 |
- аналитические данные: `target_text`, `competitors`, `keywords`, `language`, `target_title`, `competitor_titles`
|
| 168 |
- LLM: `api_key`, `api_base_url`, `model`, `temperature`
|
| 169 |
-
- стратегия: `max_iterations`, `candidates_per_iteration`, `optimization_mode`
|
| 170 |
-
- `phrase_strategy_mode`: `auto | distributed_preferred | exact_preferred | ensemble`
|
| 171 |
-
- `ensemble`: в пределах итерации циклически пробует несколько phrase-стратегий и ранжирует кандидаты общей utility-функцией.
|
| 172 |
-
- `bert_stage_target`: пользовательский порог завершения этапа A (BERT), например `0.61` вместо `0.70`.
|
| 173 |
|
| 174 |
### Выход (`OptimizerResponse`)
|
| 175 |
-
- `optimized_text`
|
| 176 |
-
- `optimized_title` — итоговая строка для поля **Title**; в ответе она берётся из снимка `title_analysis.target_title` (тот же текст, что учитывался в метрике Title BERT), с запасным вариантом из переменной оптимизатора. В `final_metrics` дополнительно есть `resolved_title` с тем же смыслом (удобно для UI/fallback).
|
| 177 |
- `baseline_metrics`, `final_metrics`
|
| 178 |
- `iterations[]` (подробный лог шагов)
|
| 179 |
- `applied_changes`
|
| 180 |
- `optimization_mode`
|
| 181 |
-
- `phrase_strategy_mode`
|
| 182 |
-
- `bert_stage_target`
|
| 183 |
-
- `stopped_early`, `stop_reason` — при ручной остановке (частичный результат)
|
| 184 |
- `error` (если есть)
|
| 185 |
|
| 186 |
-
### `POST /api/v1/optimizer/run-stream` (SSE)
|
| 187 |
-
Тело как у `run`. Поток `text/event-stream`, события `job` (с `job_id`), `preparing`, `started`, `step_start`, `llm_call`, затем `complete` с полем `result` или `error`.
|
| 188 |
-
|
| 189 |
-
### `POST /api/v1/optimizer/cancel`
|
| 190 |
-
Тело: `{"job_id": "..."}`. Только флаг отмены; клиент дочитывает SSE до `complete`.
|
| 191 |
-
|
| 192 |
-
### UI и HF/SES
|
| 193 |
-
Клиентский код: **`static/js/app.js`** + `GET /static/js/app.js`. Прогресс оптимизатора — **локальная панель с текстовым логом** (и тонкая полоса), **без** `#loader`. Подробности про SES и «мёртвые кнопки»: `docs/HF_SES_AND_UI.md`.
|
| 194 |
-
|
| 195 |
---
|
| 196 |
|
| 197 |
## 5) Подробная алгоритмика по модулям
|
|
@@ -442,15 +424,9 @@ HTML extraction pipeline:
|
|
| 442 |
|
| 443 |
### Генерация кандидатов
|
| 444 |
- `_llm_edit_chunk` — отправляет structured prompt в OpenAI-compatible API.
|
| 445 |
-
- роль модели в prompt: **semantic-vector optimizer for SEO**, а не общий “copy editor”.
|
| 446 |
- учитывает `cascade_level` и тип операции (`rewrite`/`insert`)
|
| 447 |
- явно требует грамматически корректный и естественный текст
|
| 448 |
- ограничивает число предложений по уровню
|
| 449 |
-
- для BERT динамически выбирает стратегию по длине целевой фразы:
|
| 450 |
-
- короткие цели: допустим один natural exact match;
|
| 451 |
-
- длинные multi-word цели: приоритет у distributed semantic coverage (части фразы/леммы/близк��е формулировки), без forced exact match.
|
| 452 |
-
- exact phrase не должен повторяться: при неестественном звучании он запрещается в пользу распределённой формулировки.
|
| 453 |
-
- для `rewrite` явно требует сохранить исходный смысл `sentence-by-sentence` и не менять субъект/ключевую сущность без необходимости.
|
| 454 |
|
| 455 |
### Применение правок
|
| 456 |
- `_replace_span` — замена диапазона предложений.
|
|
@@ -460,37 +436,20 @@ HTML extraction pipeline:
|
|
| 460 |
- `_goal_improved`:
|
| 461 |
- для BERT: улучшение score целевой фразы минимум на `BERT_GOAL_DELTA_MIN=0.005` **или** снижение `bert_low_count`;
|
| 462 |
- для других целей: профильные метрики улучшения.
|
| 463 |
-
- `_candidate_utility`:
|
| 464 |
-
- многоцелевая функция полезности кандидата с динамическими весами;
|
| 465 |
-
- учитывает одновременно `bert_phrase_delta`, `chunk_goal_delta`, `score_delta`;
|
| 466 |
-
- добавляет мягкие штрафы за регрессии по BM25/BERT-low/N-gram/SemanticGap/Title;
|
| 467 |
-
- в BERT-push режиме (когда фраза ниже порога) усиливает вес phrase-level прогресса.
|
| 468 |
- `_is_candidate_valid`:
|
| 469 |
- hard constraints (не ухудшать критичные метрики сверх допустимого);
|
| 470 |
- режимы `conservative/balanced/aggressive` задают пороги регрессии;
|
| 471 |
- решение учитывает и `goal_improved`, и общий `delta_score`.
|
| 472 |
-
- `_is_stage_complete` для `bert`:
|
| 473 |
-
- этап считается завершённым только когда **каждая** отслеживаемая ключевая фраза достигает `bert_stage_target` (проверка по `min(bert_phrase_scores)`);
|
| 474 |
-
- достижение порога одной «сильной» фразой больше не завершает BERT-этап.
|
| 475 |
-
- унифицированный цикл по целям: базовые параметры запроса `max_iterations` и `candidates_per_iteration` задают «якорь», но для **каждой** цели вычисляется эффективный бюджет (`_per_goal_budget`): число попыток и ширина пула кандидатов **масштабируются по дефициту** до таргета — для BERT по разрыву score до порога, для semantic по `semantic_gap`, для n-gram по отставанию/перегрузу относительно целевого счётчика, для BM25 по «лишним» вхождениям слова, для title по разрыву `title_bert_score`. После исчерпания лимита по текущей цели оптимизатор переходит к следующей цели той же стадии.
|
| 476 |
-
- `_validate_candidate_text`:
|
| 477 |
-
- отклоняет некачественные/спамные кандидаты (дубли слов/сущностей, подозрительные склейки токенов);
|
| 478 |
-
- добавляет anti-stuffing фильтр для цели BERT (повторы exact phrase и чрезмерные повторы focus-термов).
|
| 479 |
|
| 480 |
### Главная функция `optimize_text`
|
| 481 |
Итерационный цикл:
|
| 482 |
1. baseline metrics.
|
| 483 |
-
- общий бюджет шагов оценивается как **сумма эффективных итераций по всем целям** (`_estimate_total_loop_budget`: для каждой цели — `_per_goal_budget`, затем сумма по стадиям с верхней отсечкой), то есть масштабируется и по числу целей, и по величине отставания от таргета. В SSE-событии `step_start` дополнительно передаются `goal_budget_iter` и `goal_budget_candidates` для текущей цели.
|
| 484 |
2. выбрать goal.
|
| 485 |
3. выбрать пул чанков и операцию каскада.
|
| 486 |
-
- **Этап `title`:** если средняя BERT-близость Title к ключам (`title_bert_score`) ниже порога (`TITLE_TARGET_THRESHOLD` ≈ 0.65), цель — **только переписать текст из поля Title** (`target_title`), а не абзац основного текста. LLM получает текущий title, выдержку из body и ключевые слова; метрики пересчитываются с новым title. Пакетные правки по body с title не смешиваются.
|
| 487 |
-
- **Проверка деплоя:** в debug кандидата для шага `title` в `llm_prompt_debug` должно быть `"operation": "title_rewrite"`, а `chunk_text` — короткая строка текущего Title. Если видите `"operation": "rewrite"` и длинный `chunk_text` из body — на сервере старая версия `optimizer.py` (или не пересобран образ).
|
| 488 |
- на шаг выбирается несколько span-кандидатов (multi-chunk selection), а не один;
|
| 489 |
- ранжирование учитывает `focus_terms/avoid_terms`, chunk-level relevance и шумовые эвристики (menu/CTA/header penalties);
|
| 490 |
-
- для **n-gram** целей предложения ранжируются через **скользящие перекрывающиеся окна** из 2–4 предложений (шаг 1): каждому предложению присваивается лучший балл среди окон, оценка штрафует локальные повторы фразы и шумовые блоки;
|
| 491 |
-
- для BERT-целей ранжирование не ограничивается участками с already-present вхождениями: дополнительно приоритизируются релевантные участки с недопредставленными core-термами, где их можно добавить естественно;
|
| 492 |
- используется `attempt_cursor` по цели и `attempted_spans`, чтобы избежать циклов по одному и тому же участку.
|
| 493 |
-
4. сгенерировать `N` кандидатов для каждого выбранного span
|
| 494 |
5. pre-validation (формат/качество/длины).
|
| 495 |
6. chunk-level оценка:
|
| 496 |
- вычисляется `chunk_goal_delta` (релевантность чанка до/после к текущей цели);
|
|
@@ -503,7 +462,6 @@ HTML extraction pipeline:
|
|
| 503 |
- если локально улучшает чанк, но глобально не проходит — кандидат кладется в queue.
|
| 504 |
- для BERT учитывается прямой документный `bert_phrase_delta` по целевой фразе: даже небольшой положительный рост считается полезным шагом при отсутствии регрессий по guardrails.
|
| 505 |
- если нет `promotable` кандидата, но есть guardrail-valid кандидат с `local_chunk_improved`, применяется режим `applied_local_progress`: правка принимается локально и оптимизация переходит к следующему чанку (накопительная стратегия).
|
| 506 |
-
- ранжирование и выбор best-кандидата дополнительно учитывают `candidate_utility`, чтобы BERT-оптимизация не вредила следующим этапам по другим метрикам.
|
| 507 |
9. batch-логика queue:
|
| 508 |
- optimizer пробует совместно применить комбинации из 2..4 локально сильных не конфликтующих правок;
|
| 509 |
- batch принимается только при прохождении глобальных ограничений и положительном совокупном локальном приросте.
|
|
@@ -514,13 +472,12 @@ HTML extraction pipeline:
|
|
| 514 |
- `L4`: более широкий rewrite окна (до 5 предложений с вариативным охватом).
|
| 515 |
11. вести подробный лог по каждому кандидату.
|
| 516 |
- в debug-таблице фиксируются и chunk-level сигналы (`local+`, `chunk Δ`, `rel before->after`) наряду с глобальными (`Δ score`, `valid`, `goal+`);
|
| 517 |
-
- для каждого кандидата сохраняется `llm_prompt_debug` (операция, цель, фокус-термы, chunk и ближайший контекст), что позволяет анализировать фактический вход в LLM
|
| 518 |
-
- LLM возвращает поле `rationale` (1 строка) — краткое объяснение, почему правка должна повысить релевантность цели.
|
| 519 |
- также сохраняется `metrics_delta` (вклад BM25/BERT/Semantic/N-gram/Title в общий сдвиг), включая `semantic_gap_sum` и изменение состава gap-термов (`semantic_gap_terms_added/removed`), чтобы видеть, за счет чего падает или растет `score`.
|
| 520 |
|
| 521 |
---
|
| 522 |
|
| 523 |
-
## 6) Frontend (`templates/index.html`
|
| 524 |
|
| 525 |
## 6.1 Ввод данных и URL import
|
| 526 |
- `loadUserAgentOptions` — загрузка пресетов UA.
|
|
@@ -549,12 +506,9 @@ API-ключ оптимизатора в persist-состояние не сох
|
|
| 549 |
|
| 550 |
## 6.4 Сводка и оптимизатор
|
| 551 |
- `renderActionSummary` — агрегирует рекомендации BERT/BM25/N-grams/Title/Semantic в табличный формат.
|
| 552 |
-
- `runLlmOptimization` —
|
| 553 |
-
- `
|
| 554 |
-
- `optimizerLogAppend` / `applyOptimizerStreamEvent` — текстовый ход работы.
|
| 555 |
-
- `renderOptimizerResults` — итог и debug-лог; баннер при `stopped_early`.
|
| 556 |
- `applyOptimizedText` — перенос optimized текста в `target_text`.
|
| 557 |
-
- `nv(v, d)` — nullish-fallback без операторов `??` (SES на HF).
|
| 558 |
|
| 559 |
## 6.5 Сортировка таблицы мощных терминов
|
| 560 |
- `setSemanticTermSortBy`
|
|
|
|
| 24 |
- смысловой поиск по словам и фразам
|
| 25 |
- сравнение с конкурентами (включая таблицу мощных терминов)
|
| 26 |
|
| 27 |
+
3. **LLM Optimizer** (`POST /api/v1/optimizer/run`)
|
| 28 |
- итеративная локальная оптимизация текста
|
| 29 |
- многокритериальный скоринг с защитой от деградации
|
| 30 |
- каскад уровней правок (от минимальных к более широким)
|
|
|
|
| 44 |
- `search.py` — смысловой поиск в графе (фразы + слова).
|
| 45 |
- `url_fetcher.py` — извлечение текста/title из URL с выбором user-agent.
|
| 46 |
- `optimizer.py` — LLM-оптимизация с обратной связью от метрик.
|
| 47 |
+
- `templates/index.html` — frontend (UI + клиентская логика JS).
|
|
|
|
|
|
|
| 48 |
|
| 49 |
---
|
| 50 |
|
|
|
|
| 164 |
### Вход (`OptimizerRequest`)
|
| 165 |
- аналитические данные: `target_text`, `competitors`, `keywords`, `language`, `target_title`, `competitor_titles`
|
| 166 |
- LLM: `api_key`, `api_base_url`, `model`, `temperature`
|
| 167 |
+
- стратегия: `max_iterations`, `candidates_per_iteration`, `optimization_mode`
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
### Выход (`OptimizerResponse`)
|
| 170 |
+
- `optimized_text`
|
|
|
|
| 171 |
- `baseline_metrics`, `final_metrics`
|
| 172 |
- `iterations[]` (подробный лог шагов)
|
| 173 |
- `applied_changes`
|
| 174 |
- `optimization_mode`
|
|
|
|
|
|
|
|
|
|
| 175 |
- `error` (если есть)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
---
|
| 178 |
|
| 179 |
## 5) Подробная алгоритмика по модулям
|
|
|
|
| 424 |
|
| 425 |
### Генерация кандидатов
|
| 426 |
- `_llm_edit_chunk` — отправляет structured prompt в OpenAI-compatible API.
|
|
|
|
| 427 |
- учитывает `cascade_level` и тип операции (`rewrite`/`insert`)
|
| 428 |
- явно требует грамматически корректный и естественный текст
|
| 429 |
- ограничивает число предложений по уровню
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
|
| 431 |
### Применение правок
|
| 432 |
- `_replace_span` — замена диапазона предложений.
|
|
|
|
| 436 |
- `_goal_improved`:
|
| 437 |
- для BERT: улучшение score целевой фразы минимум на `BERT_GOAL_DELTA_MIN=0.005` **или** снижение `bert_low_count`;
|
| 438 |
- для других целей: профильные метрики улучшения.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
- `_is_candidate_valid`:
|
| 440 |
- hard constraints (не ухудшать критичные метрики сверх допустимого);
|
| 441 |
- режимы `conservative/balanced/aggressive` задают пороги регрессии;
|
| 442 |
- решение учитывает и `goal_improved`, и общий `delta_score`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
### Главная функция `optimize_text`
|
| 445 |
Итерационный цикл:
|
| 446 |
1. baseline metrics.
|
|
|
|
| 447 |
2. выбрать goal.
|
| 448 |
3. выбрать пул чанков и операцию каскада.
|
|
|
|
|
|
|
| 449 |
- на шаг выбирается несколько span-кандидатов (multi-chunk selection), а не один;
|
| 450 |
- ранжирование учитывает `focus_terms/avoid_terms`, chunk-level relevance и шумовые эвристики (menu/CTA/header penalties);
|
|
|
|
|
|
|
| 451 |
- используется `attempt_cursor` по цели и `attempted_spans`, чтобы избежать циклов по одному и тому же участку.
|
| 452 |
+
4. сгенерировать `N` кандидатов для каждого выбранного span.
|
| 453 |
5. pre-validation (формат/качество/длины).
|
| 454 |
6. chunk-level оценка:
|
| 455 |
- вычисляется `chunk_goal_delta` (релевантность чанка до/после к текущей цели);
|
|
|
|
| 462 |
- если локально улучшает чанк, но глобально не проходит — кандидат кладется в queue.
|
| 463 |
- для BERT учитывается прямой документный `bert_phrase_delta` по целевой фразе: даже небольшой положительный рост считается полезным шагом при отсутствии регрессий по guardrails.
|
| 464 |
- если нет `promotable` кандидата, но есть guardrail-valid кандидат с `local_chunk_improved`, применяется режим `applied_local_progress`: правка принимается локально и оптимизация переходит к следующему чанку (накопительная стратегия).
|
|
|
|
| 465 |
9. batch-логика queue:
|
| 466 |
- optimizer пробует совместно применить комбинации из 2..4 локально сильных не конфликтующих правок;
|
| 467 |
- batch принимается только при прохождении глобальных ограничений и положительном совокупном локальном приросте.
|
|
|
|
| 472 |
- `L4`: более широкий rewrite окна (до 5 предложений с вариативным охватом).
|
| 473 |
11. вести подробный лог по каждому кандидату.
|
| 474 |
- в debug-таблице фиксируются и chunk-level сигналы (`local+`, `chunk Δ`, `rel before->after`) наряду с глобальными (`Δ score`, `valid`, `goal+`);
|
| 475 |
+
- для каждого кандидата сохраняется `llm_prompt_debug` (операция, цель, фокус-термы, chunk и ближайший контекст), что позволяет анализировать фактический вход в LLM.
|
|
|
|
| 476 |
- также сохраняется `metrics_delta` (вклад BM25/BERT/Semantic/N-gram/Title в общий сдвиг), включая `semantic_gap_sum` и изменение состава gap-термов (`semantic_gap_terms_added/removed`), чтобы видеть, за счет чего падает или растет `score`.
|
| 477 |
|
| 478 |
---
|
| 479 |
|
| 480 |
+
## 6) Frontend (`templates/index.html`) — сценарии и функции
|
| 481 |
|
| 482 |
## 6.1 Ввод данных и URL import
|
| 483 |
- `loadUserAgentOptions` — загрузка пресетов UA.
|
|
|
|
| 506 |
|
| 507 |
## 6.4 Сводка и оптимизатор
|
| 508 |
- `renderActionSummary` — агрегирует рекомендации BERT/BM25/N-grams/Title/Semantic в табличный формат.
|
| 509 |
+
- `runLlmOptimization` — запуск оптимизации.
|
| 510 |
+
- `renderOptimizerResults` — итог и debug-лог по шагам/кандидатам.
|
|
|
|
|
|
|
| 511 |
- `applyOptimizedText` — перенос optimized текста в `target_text`.
|
|
|
|
| 512 |
|
| 513 |
## 6.5 Сортировка таблицы мощных терминов
|
| 514 |
- `setSemanticTermSortBy`
|
docs/HF_SES_AND_UI.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
# Почему на Hugging Face «умирали» все кнопки (версии с прогресс-баром)
|
| 2 |
-
|
| 3 |
-
По **фактам из консоли** (не гипотезы):
|
| 4 |
-
|
| 5 |
-
## 1. `SES_UNCAUGHT_EXCEPTION: SyntaxError: missing : in conditional expression`
|
| 6 |
-
|
| 7 |
-
На странице Space подключается **SES lockdown** (`lockdown-install.js`). Его парсер/конвейер для кода страницы **не эквивалентен** последнему движку Firefox/Chrome в части синтаксиса ES2020+.
|
| 8 |
-
|
| 9 |
-
- Операторы **`??` (nullish coalescing)** и **`?.` (optional chaining)** в **одном исходнике** с большим inline-`<script>` давали ошибку разбора, интерпретируемую как **сломанный тернарный `? … :`** → *missing ':'*.
|
| 10 |
-
- После этой ошибки **весь** клиентский скрипт приложения **не выполняется** → не регистрируются **ни** `onclick`, **ни** делегирование `data-app-action` → кажется, что «сломались все кнопки».
|
| 11 |
-
|
| 12 |
-
**Исправление:** не использовать `??` / `?.` в коде приложения; вместо них — функция `nv(v, d)` и проверки `obj && obj.prop`.
|
| 13 |
-
|
| 14 |
-
## 2. Глобальный `#loader`
|
| 15 |
-
|
| 16 |
-
Если на время оптимизатора включать полноэкранный оверлей и по какой-то причине не снять `display` в `finally`, **все клики** уходят в оверлей. Это уже не SES, а логика UI.
|
| 17 |
-
|
| 18 |
-
**Исправление:** во время LLM-оптимизации **не** трогать `#loader`; прогресс только в **локальной панели** под кнопками.
|
| 19 |
-
|
| 20 |
-
## 3. Строки CSP про `inpage.js`, Stripe, `content.js`
|
| 21 |
-
|
| 22 |
-
В логе часто идут **расширения браузера** и iframe HF, а не ваш `index.html`. На диагностику кнопок приложения они обычно не влияют.
|
| 23 |
-
|
| 24 |
-
## Текущая схема (после правок)
|
| 25 |
-
|
| 26 |
-
- Логика UI в **`/static/js/app.js`** (отдельный файл, не гигантский inline).
|
| 27 |
-
- Прогресс оптимизатора: **панель с `<pre>`-логом** + тонкая полоса; **без** блокировки всего экрана.
|
| 28 |
-
- Поток **`/api/v1/optimizer/run-stream`** (SSE) + **`/api/v1/optimizer/cancel`** для остановки с частичным результатом.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/TEXT_OPTIMIZER_PRINCIPLES.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
# Text Optimizer Principles
|
| 2 |
-
|
| 3 |
-
This document is a living spec for iterative text optimization behavior.
|
| 4 |
-
Update it whenever optimization policy changes.
|
| 5 |
-
|
| 6 |
-
## 1) Multi-objective optimization model
|
| 7 |
-
|
| 8 |
-
- **Primary objective (by stage):**
|
| 9 |
-
- Stage A: BERT phrase relevance
|
| 10 |
-
- Stage B: BM25 remove cleanup
|
| 11 |
-
- Stage C: N-gram balancing
|
| 12 |
-
- Stage D: Semantic gap balancing
|
| 13 |
-
- Stage E: Title alignment
|
| 14 |
-
- **Guardrails (always active):**
|
| 15 |
-
- Do not allow critical metric regressions beyond mode tolerances.
|
| 16 |
-
- Keep grammar, coherence, and non-spam writing.
|
| 17 |
-
|
| 18 |
-
## 2) Stage order and skipping
|
| 19 |
-
|
| 20 |
-
- Stage order:
|
| 21 |
-
- `bert -> bm25 -> ngram -> semantic -> title`
|
| 22 |
-
- A stage is skipped if no actionable goal exists.
|
| 23 |
-
- Plateau rule:
|
| 24 |
-
- If no primary progress for 3 steps, move to next stage.
|
| 25 |
-
|
| 26 |
-
## 3) BERT stage policy
|
| 27 |
-
|
| 28 |
-
- Default Stage A threshold: `0.70`.
|
| 29 |
-
- User may set custom threshold via UI (`BERT target A-stage`), e.g. `0.61`.
|
| 30 |
-
- Stage A is complete when max target phrase score reaches configured threshold.
|
| 31 |
-
|
| 32 |
-
## 4) BM25 stage policy
|
| 33 |
-
|
| 34 |
-
- Main target: reduce/remove over-optimization signals.
|
| 35 |
-
- A stage is considered healthy when `bm25_remove_count <= 3`.
|
| 36 |
-
|
| 37 |
-
## 5) N-gram stage policy (quantitative)
|
| 38 |
-
|
| 39 |
-
- Goal: bring target counts closer to competitor average, not force exact equality.
|
| 40 |
-
- Tolerance bands:
|
| 41 |
-
- if `avg >= 4`: acceptable range is `avg +/- 20%`
|
| 42 |
-
- if `avg < 4`: acceptable range is `avg +/- 50%`
|
| 43 |
-
- N-gram signal is counted only when term is outside tolerance and present in enough competitors.
|
| 44 |
-
- Selection rules (multi-competitor mode, `competitors > 1`):
|
| 45 |
-
- bi-grams and tri-grams are eligible when present in `>= 2` competitors;
|
| 46 |
-
- unigrams are eligible only if they are part of user keyword phrases and present in `>= 2` competitors.
|
| 47 |
-
- Target ranking (which n-gram to work on next):
|
| 48 |
-
- sort eligible **underrepresented** rows by **Freq(K)** (`comp_occurrence`) descending,
|
| 49 |
-
then **Avg(K)** (`competitor_avg`) descending,
|
| 50 |
-
then **deviation** from competitor average descending (larger gap first).
|
| 51 |
-
- Iteration behavior:
|
| 52 |
-
- optimizer works on one n-gram target at a time per step;
|
| 53 |
-
- per eligible n-gram target it allocates `3` attempts, then moves to the next target;
|
| 54 |
-
- if target list ends, stage advances to the next optimization stage.
|
| 55 |
-
- **Global step budget:** the UI `max_iterations` cap still limits total loop iterations, but the
|
| 56 |
-
optimizer **adds** extra steps reserved for the n-gram stage (`targets × 3`, capped) so a low
|
| 57 |
-
`max_iterations` value does not stop the run after only three n-gram rows while many targets remain.
|
| 58 |
-
- **Chunk selection (n-gram stage):** candidate sentences are ranked using **overlapping multi-sentence
|
| 59 |
-
windows** (stride 1). Each sentence receives the best window score; windows favor low local phrase
|
| 60 |
-
duplication, topical overlap with phrase tokens, and non-noisy prose. Document-level phrase count
|
| 61 |
-
remains the primary acceptance signal.
|
| 62 |
-
|
| 63 |
-
## 5.1 Summary logic memory (current)
|
| 64 |
-
|
| 65 |
-
- Summary recommendation triggers:
|
| 66 |
-
- BERT warning when phrase score `< 0.70`;
|
| 67 |
-
- BM25 warning when `REMOVE >= 4`;
|
| 68 |
-
- N-gram warning when term is underrepresented among competitors;
|
| 69 |
-
- Title warning when Title BERT `< 0.65`;
|
| 70 |
-
- Semantic warning when keyword terms are weaker than competitor average.
|
| 71 |
-
- For N-grams in summary:
|
| 72 |
-
- summary renders top rows for readability, but optimizer runs against the full eligible candidate set.
|
| 73 |
-
|
| 74 |
-
## 6) Local acceptance and batch accumulation
|
| 75 |
-
|
| 76 |
-
- First evaluate candidate locally (chunk-level), then globally (document-level).
|
| 77 |
-
- Locally improved candidates may be queued when global score does not move yet.
|
| 78 |
-
- Non-conflicting queued edits can be applied as a batch (2-4 edits) if guardrails pass.
|
| 79 |
-
|
| 80 |
-
## 7) Text quality constraints
|
| 81 |
-
|
| 82 |
-
- Reject candidates with:
|
| 83 |
-
- duplicated entities/words,
|
| 84 |
-
- suspicious token joins,
|
| 85 |
-
- excessive sentence count for current cascade level,
|
| 86 |
-
- obvious stuffing/redundancy.
|
| 87 |
-
- Keep narrative continuity and original subject/entity focus.
|
| 88 |
-
|
| 89 |
-
## 8) Diagnostics requirements
|
| 90 |
-
|
| 91 |
-
- For every iteration, store:
|
| 92 |
-
- stage, goal, cascade level,
|
| 93 |
-
- candidate validity, local improvement, metric deltas,
|
| 94 |
-
- selected strategy and prompt debug payload.
|
| 95 |
-
- UI must show:
|
| 96 |
-
- stage progression,
|
| 97 |
-
- stage transitions,
|
| 98 |
-
- candidate strategy and reason for rejection.
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models.py
CHANGED
|
@@ -82,12 +82,6 @@ class OptimizerRequest(BaseModel):
|
|
| 82 |
language: str = "en"
|
| 83 |
target_title: str = ""
|
| 84 |
competitor_titles: List[str] = Field(default_factory=list)
|
| 85 |
-
# Base for highlighting what changed in this optimization run.
|
| 86 |
-
# - diff_from_input: compare with `target_text` passed in this request (snapshot before optimization)
|
| 87 |
-
# - diff_from_original: compare with `original_target_text` from the first snapshot in session
|
| 88 |
-
diff_mode: str = "diff_from_input" # diff_from_input | diff_from_original
|
| 89 |
-
original_target_text: Optional[str] = None
|
| 90 |
-
original_target_title: Optional[str] = None
|
| 91 |
|
| 92 |
api_key: str
|
| 93 |
api_base_url: str = "https://api.deepseek.com/v1"
|
|
@@ -97,41 +91,14 @@ class OptimizerRequest(BaseModel):
|
|
| 97 |
candidates_per_iteration: int = 2
|
| 98 |
temperature: float = 0.25
|
| 99 |
optimization_mode: str = "balanced"
|
| 100 |
-
phrase_strategy_mode: str = "auto" # auto | exact_preferred | distributed_preferred | ensemble
|
| 101 |
-
bert_stage_target: float = 0.70
|
| 102 |
-
# Optional stage control. If empty -> default full pipeline order.
|
| 103 |
-
enabled_stages: List[str] = Field(default_factory=list) # bert|bm25|ngram|semantic|title
|
| 104 |
-
# Per-stage manual goal selection and custom additions.
|
| 105 |
-
# Example:
|
| 106 |
-
# {
|
| 107 |
-
# "bm25": {"mode":"mixed","selected":["canadian online casino"],"custom_add":["online casinos canada"]},
|
| 108 |
-
# "bert": {"mode":"manual","selected":["best payout casinos"],"custom_add":[]}
|
| 109 |
-
# }
|
| 110 |
-
stage_goal_overrides: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
| 111 |
|
| 112 |
|
| 113 |
class OptimizerResponse(BaseModel):
|
| 114 |
ok: bool = True
|
| 115 |
optimized_text: str = ""
|
| 116 |
-
optimized_title: str = ""
|
| 117 |
baseline_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 118 |
final_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 119 |
iterations: List[Dict[str, Any]] = Field(default_factory=list)
|
| 120 |
applied_changes: int = 0
|
| 121 |
optimization_mode: str = "balanced"
|
| 122 |
-
|
| 123 |
-
bert_stage_target: float = 0.70
|
| 124 |
-
diff_mode: str = ""
|
| 125 |
-
# HTML with <mark class="diff-changed"> around changed parts.
|
| 126 |
-
diff_body_html: str = ""
|
| 127 |
-
diff_title_html: str = ""
|
| 128 |
-
# List of (type/from/to) blocks for "что именно поменять".
|
| 129 |
-
diff_changes: List[Dict[str, str]] = Field(default_factory=list)
|
| 130 |
-
diff_title_changes: List[Dict[str, str]] = Field(default_factory=list)
|
| 131 |
-
error: str = ""
|
| 132 |
-
stopped_early: bool = False
|
| 133 |
-
stop_reason: str = ""
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
class OptimizerCancelRequest(BaseModel):
|
| 137 |
-
job_id: str = Field(..., min_length=8)
|
|
|
|
| 82 |
language: str = "en"
|
| 83 |
target_title: str = ""
|
| 84 |
competitor_titles: List[str] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
api_key: str
|
| 87 |
api_base_url: str = "https://api.deepseek.com/v1"
|
|
|
|
| 91 |
candidates_per_iteration: int = 2
|
| 92 |
temperature: float = 0.25
|
| 93 |
optimization_mode: str = "balanced"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
|
| 96 |
class OptimizerResponse(BaseModel):
|
| 97 |
ok: bool = True
|
| 98 |
optimized_text: str = ""
|
|
|
|
| 99 |
baseline_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 100 |
final_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 101 |
iterations: List[Dict[str, Any]] = Field(default_factory=list)
|
| 102 |
applied_changes: int = 0
|
| 103 |
optimization_mode: str = "balanced"
|
| 104 |
+
error: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/app.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
templates/index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|