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, StreamingResponse
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` или в UI — SSE `run-stream`)
28
  - итеративная локальная оптимизация текста
29
  - многокритериальный скоринг с защитой от деградации
30
  - каскад уровней правок (от минимальных к более широким)
@@ -44,9 +44,7 @@
44
  - `search.py` — смысловой поиск в графе (фразы + слова).
45
  - `url_fetcher.py` — извлечение текста/title из URL с выбором user-agent.
46
  - `optimizer.py` — LLM-оптимизация с обратной связью от метрик.
47
- - `docs/TEXT_OPTIMIZER_PRINCIPLES.md` — живой регламент принципов оптимизатора (stage-пайплайн, допуски, guardrails).
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`, `phrase_strategy_mode`, `bert_stage_target`
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` — итоговый body (target)
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 (`N` зависит от эффективного бюджета кандидатов для цели и каскада, см. `_per_goal_budget` и деление по 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` + `static/js/app.js`)
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` — `POST /api/v1/optimizer/run-stream` (SSE); локальная панель **лога** + тонкий progress bar; **без** `#loader`.
553
- - `requestStopOptimizer` — `POST /api/v1/optimizer/cancel`; поток дочитывается до `complete` астичный результат).
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
- phrase_strategy_mode: str = "auto"
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