lsdf commited on
Commit
84ebb46
·
1 Parent(s): f8eb22b

Add configurable BERT stage target and stage visibility in optimizer.

Browse files

Allow users to set custom BERT threshold for Stage A completion (e.g. accept 0.61), persist it in UI project state, and expose stage transitions in logs for transparent progression to BM25/Semantic/N-gram/Title.

Made-with: Cursor

docs/FULL_FUNCTIONAL_DOCUMENTATION.md CHANGED
@@ -164,9 +164,10 @@
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`, `phrase_strategy_mode`
168
  - `phrase_strategy_mode`: `auto | distributed_preferred | exact_preferred | ensemble`
169
  - `ensemble`: в пределах итерации циклически пробует несколько phrase-стратегий и ранжирует кандидаты общей utility-функцией.
 
170
 
171
  ### Выход (`OptimizerResponse`)
172
  - `optimized_text`
@@ -175,6 +176,7 @@
175
  - `applied_changes`
176
  - `optimization_mode`
177
  - `phrase_strategy_mode`
 
178
  - `error` (если есть)
179
 
180
  ---
 
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`, `phrase_strategy_mode`, `bert_stage_target`
168
  - `phrase_strategy_mode`: `auto | distributed_preferred | exact_preferred | ensemble`
169
  - `ensemble`: в пределах итерации циклически пробует несколько phrase-стратегий и ранжирует кандидаты общей utility-функцией.
170
+ - `bert_stage_target`: пользовательский порог завершения этапа A (BERT), например `0.61` вместо `0.70`.
171
 
172
  ### Выход (`OptimizerResponse`)
173
  - `optimized_text`
 
176
  - `applied_changes`
177
  - `optimization_mode`
178
  - `phrase_strategy_mode`
179
+ - `bert_stage_target`
180
  - `error` (если есть)
181
 
182
  ---
models.py CHANGED
@@ -92,6 +92,7 @@ class OptimizerRequest(BaseModel):
92
  temperature: float = 0.25
93
  optimization_mode: str = "balanced"
94
  phrase_strategy_mode: str = "auto" # auto | exact_preferred | distributed_preferred | ensemble
 
95
 
96
 
97
  class OptimizerResponse(BaseModel):
@@ -103,4 +104,5 @@ class OptimizerResponse(BaseModel):
103
  applied_changes: int = 0
104
  optimization_mode: str = "balanced"
105
  phrase_strategy_mode: str = "auto"
 
106
  error: str = ""
 
92
  temperature: float = 0.25
93
  optimization_mode: str = "balanced"
94
  phrase_strategy_mode: str = "auto" # auto | exact_preferred | distributed_preferred | ensemble
95
+ bert_stage_target: float = 0.70
96
 
97
 
98
  class OptimizerResponse(BaseModel):
 
104
  applied_changes: int = 0
105
  optimization_mode: str = "balanced"
106
  phrase_strategy_mode: str = "auto"
107
+ bert_stage_target: float = 0.70
108
  error: str = ""
optimizer.py CHANGED
@@ -232,12 +232,18 @@ def _is_semantic_gap(target_weight: float, competitor_avg_weight: float) -> bool
232
  return (competitor_avg_weight > rel_threshold) and (abs_gap >= SEMANTIC_GAP_MIN_ABS)
233
 
234
 
235
- def _compute_metrics(analysis: Dict[str, Any], semantic: Dict[str, Any], keywords: List[str], language: str) -> Dict[str, Any]:
 
 
 
 
 
 
236
  competitor_count = len(analysis.get("word_counts", {}).get("competitors", []))
237
  min_signal = 1 if competitor_count <= 1 else 2
238
 
239
  bert_details = analysis.get("bert_analysis", {}).get("detailed", []) or []
240
- bert_low = [d for d in bert_details if float(d.get("my_max_score", 0)) < BERT_TARGET_THRESHOLD]
241
  bert_phrase_scores = {
242
  str(d.get("phrase", "")).strip().lower(): float(d.get("my_max_score", 0) or 0.0)
243
  for d in bert_details
@@ -346,10 +352,11 @@ def _choose_optimization_goal(
346
  keywords: List[str],
347
  language: str,
348
  stage: str = "bert",
 
349
  ) -> Dict[str, Any]:
350
  candidates: Dict[str, Dict[str, Any]] = {}
351
  bert_details = analysis.get("bert_analysis", {}).get("detailed", []) or []
352
- low_bert = [x for x in bert_details if float(x.get("my_max_score", 0)) < BERT_TARGET_THRESHOLD]
353
  if low_bert:
354
  worst = sorted(low_bert, key=lambda x: float(x.get("my_max_score", 0)))[0]
355
  focus_terms = _filter_stopwords(_tokenize(worst.get("phrase", "")), language)[:4]
@@ -963,9 +970,11 @@ def _stage_primary_progress(stage: str, prev_metrics: Dict[str, Any], next_metri
963
  return False
964
 
965
 
966
- def _is_stage_complete(stage: str, metrics: Dict[str, Any]) -> bool:
967
  if stage == "bert":
968
- return int(metrics.get("bert_low_count", 0)) == 0
 
 
969
  if stage == "bm25":
970
  return int(metrics.get("bm25_remove_count", 0)) <= 3
971
  if stage == "semantic":
@@ -1089,12 +1098,16 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1089
  phrase_strategy_mode = str(request_data.get("phrase_strategy_mode", "auto") or "auto").strip().lower()
1090
  if phrase_strategy_mode not in {"auto", "exact_preferred", "distributed_preferred", "ensemble"}:
1091
  phrase_strategy_mode = "auto"
 
 
1092
 
1093
  baseline_analysis = _build_analysis_snapshot(
1094
  target_text, competitors, keywords, language, target_title, competitor_titles
1095
  )
1096
  baseline_semantic = _build_semantic_snapshot(target_text, competitors, language)
1097
- baseline_metrics = _compute_metrics(baseline_analysis, baseline_semantic, keywords, language)
 
 
1098
 
1099
  current_text = target_text
1100
  current_analysis = baseline_analysis
@@ -1112,7 +1125,9 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1112
  stage_no_progress_steps = 0
1113
 
1114
  for step in range(max_iterations):
1115
- while stage_idx < len(STAGE_ORDER) and _is_stage_complete(STAGE_ORDER[stage_idx], current_metrics):
 
 
1116
  stage_idx += 1
1117
  stage_no_progress_steps = 0
1118
  if stage_idx >= len(STAGE_ORDER):
@@ -1126,6 +1141,7 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1126
  keywords,
1127
  language,
1128
  stage=active_stage,
 
1129
  )
1130
  if goal["type"] == "none":
1131
  stage_idx += 1
@@ -1314,7 +1330,9 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1314
  candidate_text, competitors, keywords, language, target_title, competitor_titles
1315
  )
1316
  cand_semantic = _build_semantic_snapshot(candidate_text, competitors, language)
1317
- cand_metrics = _compute_metrics(cand_analysis, cand_semantic, keywords, language)
 
 
1318
  valid, invalid_reasons, goal_improved = _is_candidate_valid(
1319
  current_metrics, cand_metrics, goal["type"], goal["label"], optimization_mode
1320
  )
@@ -1548,7 +1566,9 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1548
  batch_text, competitors, keywords, language, target_title, competitor_titles
1549
  )
1550
  batch_semantic = _build_semantic_snapshot(batch_text, competitors, language)
1551
- batch_metrics = _compute_metrics(batch_analysis, batch_semantic, keywords, language)
 
 
1552
  b_valid, b_reasons, b_goal = _is_candidate_valid(
1553
  current_metrics, batch_metrics, goal["type"], goal["label"], optimization_mode
1554
  )
@@ -1801,4 +1821,5 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
1801
  "applied_changes": applied_changes,
1802
  "optimization_mode": optimization_mode,
1803
  "phrase_strategy_mode": phrase_strategy_mode,
 
1804
  }
 
232
  return (competitor_avg_weight > rel_threshold) and (abs_gap >= SEMANTIC_GAP_MIN_ABS)
233
 
234
 
235
+ def _compute_metrics(
236
+ analysis: Dict[str, Any],
237
+ semantic: Dict[str, Any],
238
+ keywords: List[str],
239
+ language: str,
240
+ bert_stage_target: float = BERT_TARGET_THRESHOLD,
241
+ ) -> Dict[str, Any]:
242
  competitor_count = len(analysis.get("word_counts", {}).get("competitors", []))
243
  min_signal = 1 if competitor_count <= 1 else 2
244
 
245
  bert_details = analysis.get("bert_analysis", {}).get("detailed", []) or []
246
+ bert_low = [d for d in bert_details if float(d.get("my_max_score", 0)) < float(bert_stage_target)]
247
  bert_phrase_scores = {
248
  str(d.get("phrase", "")).strip().lower(): float(d.get("my_max_score", 0) or 0.0)
249
  for d in bert_details
 
352
  keywords: List[str],
353
  language: str,
354
  stage: str = "bert",
355
+ bert_stage_target: float = BERT_TARGET_THRESHOLD,
356
  ) -> Dict[str, Any]:
357
  candidates: Dict[str, Dict[str, Any]] = {}
358
  bert_details = analysis.get("bert_analysis", {}).get("detailed", []) or []
359
+ low_bert = [x for x in bert_details if float(x.get("my_max_score", 0)) < float(bert_stage_target)]
360
  if low_bert:
361
  worst = sorted(low_bert, key=lambda x: float(x.get("my_max_score", 0)))[0]
362
  focus_terms = _filter_stopwords(_tokenize(worst.get("phrase", "")), language)[:4]
 
970
  return False
971
 
972
 
973
+ def _is_stage_complete(stage: str, metrics: Dict[str, Any], bert_stage_target: float = BERT_TARGET_THRESHOLD) -> bool:
974
  if stage == "bert":
975
+ scores = [float(v) for v in (metrics.get("bert_phrase_scores") or {}).values()]
976
+ max_phrase = max([0.0] + scores)
977
+ return max_phrase >= float(bert_stage_target)
978
  if stage == "bm25":
979
  return int(metrics.get("bm25_remove_count", 0)) <= 3
980
  if stage == "semantic":
 
1098
  phrase_strategy_mode = str(request_data.get("phrase_strategy_mode", "auto") or "auto").strip().lower()
1099
  if phrase_strategy_mode not in {"auto", "exact_preferred", "distributed_preferred", "ensemble"}:
1100
  phrase_strategy_mode = "auto"
1101
+ bert_stage_target = float(request_data.get("bert_stage_target", BERT_TARGET_THRESHOLD) or BERT_TARGET_THRESHOLD)
1102
+ bert_stage_target = max(0.0, min(1.0, bert_stage_target))
1103
 
1104
  baseline_analysis = _build_analysis_snapshot(
1105
  target_text, competitors, keywords, language, target_title, competitor_titles
1106
  )
1107
  baseline_semantic = _build_semantic_snapshot(target_text, competitors, language)
1108
+ baseline_metrics = _compute_metrics(
1109
+ baseline_analysis, baseline_semantic, keywords, language, bert_stage_target=bert_stage_target
1110
+ )
1111
 
1112
  current_text = target_text
1113
  current_analysis = baseline_analysis
 
1125
  stage_no_progress_steps = 0
1126
 
1127
  for step in range(max_iterations):
1128
+ while stage_idx < len(STAGE_ORDER) and _is_stage_complete(
1129
+ STAGE_ORDER[stage_idx], current_metrics, bert_stage_target=bert_stage_target
1130
+ ):
1131
  stage_idx += 1
1132
  stage_no_progress_steps = 0
1133
  if stage_idx >= len(STAGE_ORDER):
 
1141
  keywords,
1142
  language,
1143
  stage=active_stage,
1144
+ bert_stage_target=bert_stage_target,
1145
  )
1146
  if goal["type"] == "none":
1147
  stage_idx += 1
 
1330
  candidate_text, competitors, keywords, language, target_title, competitor_titles
1331
  )
1332
  cand_semantic = _build_semantic_snapshot(candidate_text, competitors, language)
1333
+ cand_metrics = _compute_metrics(
1334
+ cand_analysis, cand_semantic, keywords, language, bert_stage_target=bert_stage_target
1335
+ )
1336
  valid, invalid_reasons, goal_improved = _is_candidate_valid(
1337
  current_metrics, cand_metrics, goal["type"], goal["label"], optimization_mode
1338
  )
 
1566
  batch_text, competitors, keywords, language, target_title, competitor_titles
1567
  )
1568
  batch_semantic = _build_semantic_snapshot(batch_text, competitors, language)
1569
+ batch_metrics = _compute_metrics(
1570
+ batch_analysis, batch_semantic, keywords, language, bert_stage_target=bert_stage_target
1571
+ )
1572
  b_valid, b_reasons, b_goal = _is_candidate_valid(
1573
  current_metrics, batch_metrics, goal["type"], goal["label"], optimization_mode
1574
  )
 
1821
  "applied_changes": applied_changes,
1822
  "optimization_mode": optimization_mode,
1823
  "phrase_strategy_mode": phrase_strategy_mode,
1824
+ "bert_stage_target": round(bert_stage_target, 4),
1825
  }
templates/index.html CHANGED
@@ -302,6 +302,10 @@
302
  <label class="form-label small text-muted mb-1">Temp</label>
303
  <input type="number" id="optimizerTemp" class="form-control" min="0" max="1.2" step="0.05" value="0.25">
304
  </div>
 
 
 
 
305
  <div class="col-md-3">
306
  <label class="form-label small text-muted mb-1">Режим оптимизации</label>
307
  <select id="optimizerMode" class="form-select">
@@ -558,6 +562,7 @@
558
  optimizer_iterations: Number(document.getElementById('optimizerIterations').value || 2),
559
  optimizer_candidates: Number(document.getElementById('optimizerCandidates').value || 2),
560
  optimizer_temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
 
561
  optimizer_mode: document.getElementById('optimizerMode').value,
562
  optimizer_phrase_strategy: document.getElementById('optimizerPhraseStrategy').value
563
  },
@@ -609,6 +614,7 @@
609
  document.getElementById('optimizerIterations').value = 2;
610
  document.getElementById('optimizerCandidates').value = 2;
611
  document.getElementById('optimizerTemp').value = 0.25;
 
612
  document.getElementById('optimizerMode').value = 'balanced';
613
  document.getElementById('optimizerPhraseStrategy').value = 'auto';
614
 
@@ -663,6 +669,7 @@
663
  document.getElementById('optimizerIterations').value = inp.optimizer_iterations ?? 2;
664
  document.getElementById('optimizerCandidates').value = inp.optimizer_candidates ?? 2;
665
  document.getElementById('optimizerTemp').value = inp.optimizer_temperature ?? 0.25;
 
666
  document.getElementById('optimizerMode').value = inp.optimizer_mode || 'balanced';
667
  document.getElementById('optimizerPhraseStrategy').value = inp.optimizer_phrase_strategy || 'auto';
668
 
@@ -853,9 +860,12 @@
853
  const after = it.metrics_after ? it.metrics_after.score : '-';
854
  const baseline = (it.current_score ?? before);
855
  const reason = it.reason || (it.candidates ? 'all candidates rejected by constraints' : '-');
 
 
856
  return `<tr>
857
  <td>${it.step}</td>
858
  <td>${it.status}</td>
 
859
  <td>${it.goal ? (it.goal.type + ': ' + (it.goal.label || '')) : '-'}</td>
860
  <td>L${it.cascade_level || '-'} / ${it.operation || '-'}</td>
861
  <td>${baseline}</td>
@@ -919,9 +929,11 @@
919
  <div><strong>Шаг ${it.step}</strong> — ${safeHtml(it.status || '-')}</div>
920
  <span class="badge bg-secondary">${safeHtml(it.goal ? (it.goal.type + ': ' + (it.goal.label || '')) : '-')}</span>
921
  </div>
 
922
  <div class="small mb-2"><strong>Каскад:</strong> L${safeHtml(it.cascade_level || '-')} / ${safeHtml(it.operation || '-')}</div>
923
  <div class="small mb-2"><strong>Причина:</strong> ${safeHtml(it.reason || '-')}</div>
924
  ${it.escalated_to_level ? `<div class="small mb-2 text-warning"><strong>Эскалация:</strong> переход на L${safeHtml(it.escalated_to_level)}</div>` : ''}
 
925
  <div class="small mb-2"><strong>Исходное предложение:</strong><br><span class="text-muted">${sentBefore}</span></div>
926
  <div class="small mb-2"><strong>Выбранный вариант:</strong><br><span class="text-muted">${sentAfterChosen}</span></div>
927
  <div class="table-responsive">
@@ -945,6 +957,7 @@
945
  <div class="small mb-2">
946
  Режим: <strong>${data.optimization_mode || 'balanced'}</strong>
947
  · Phrase Strategy: <strong>${data.phrase_strategy_mode || 'auto'}</strong>
 
948
  </div>
949
  <div class="table-responsive">
950
  <table class="table table-sm table-bordered mb-0">
@@ -957,8 +970,8 @@
957
  <h6 class="card-title">Лог итераций</h6>
958
  <div class="table-responsive">
959
  <table class="table table-sm table-hover mb-0">
960
- <thead><tr><th>#</th><th>Статус</th><th>Цель</th><th>Каскад</th><th>Baseline шага</th><th>Score до</th><th>Score после</th><th>Δ</th><th>Причина/комментарий</th></tr></thead>
961
- <tbody>${iterRows || '<tr><td colspan="9" class="text-muted text-center">Нет данных</td></tr>'}</tbody>
962
  </table>
963
  </div>
964
  </div>
@@ -1002,6 +1015,7 @@
1002
  max_iterations: Number(document.getElementById('optimizerIterations').value || 2),
1003
  candidates_per_iteration: Number(document.getElementById('optimizerCandidates').value || 2),
1004
  temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
 
1005
  optimization_mode: document.getElementById('optimizerMode').value || 'balanced',
1006
  phrase_strategy_mode: document.getElementById('optimizerPhraseStrategy').value || 'auto'
1007
  };
 
302
  <label class="form-label small text-muted mb-1">Temp</label>
303
  <input type="number" id="optimizerTemp" class="form-control" min="0" max="1.2" step="0.05" value="0.25">
304
  </div>
305
+ <div class="col-md-2">
306
+ <label class="form-label small text-muted mb-1">BERT target A-stage</label>
307
+ <input type="number" id="optimizerBertStageTarget" class="form-control" min="0" max="1" step="0.01" value="0.70">
308
+ </div>
309
  <div class="col-md-3">
310
  <label class="form-label small text-muted mb-1">Режим оптимизации</label>
311
  <select id="optimizerMode" class="form-select">
 
562
  optimizer_iterations: Number(document.getElementById('optimizerIterations').value || 2),
563
  optimizer_candidates: Number(document.getElementById('optimizerCandidates').value || 2),
564
  optimizer_temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
565
+ optimizer_bert_stage_target: Number(document.getElementById('optimizerBertStageTarget').value || 0.70),
566
  optimizer_mode: document.getElementById('optimizerMode').value,
567
  optimizer_phrase_strategy: document.getElementById('optimizerPhraseStrategy').value
568
  },
 
614
  document.getElementById('optimizerIterations').value = 2;
615
  document.getElementById('optimizerCandidates').value = 2;
616
  document.getElementById('optimizerTemp').value = 0.25;
617
+ document.getElementById('optimizerBertStageTarget').value = 0.70;
618
  document.getElementById('optimizerMode').value = 'balanced';
619
  document.getElementById('optimizerPhraseStrategy').value = 'auto';
620
 
 
669
  document.getElementById('optimizerIterations').value = inp.optimizer_iterations ?? 2;
670
  document.getElementById('optimizerCandidates').value = inp.optimizer_candidates ?? 2;
671
  document.getElementById('optimizerTemp').value = inp.optimizer_temperature ?? 0.25;
672
+ document.getElementById('optimizerBertStageTarget').value = inp.optimizer_bert_stage_target ?? 0.70;
673
  document.getElementById('optimizerMode').value = inp.optimizer_mode || 'balanced';
674
  document.getElementById('optimizerPhraseStrategy').value = inp.optimizer_phrase_strategy || 'auto';
675
 
 
860
  const after = it.metrics_after ? it.metrics_after.score : '-';
861
  const baseline = (it.current_score ?? before);
862
  const reason = it.reason || (it.candidates ? 'all candidates rejected by constraints' : '-');
863
+ const stage = (it.stage || (it.goal && it.goal.type) || '-');
864
+ const advanced = it.advanced_to_stage ? ` → ${it.advanced_to_stage}` : '';
865
  return `<tr>
866
  <td>${it.step}</td>
867
  <td>${it.status}</td>
868
+ <td>${stage}${advanced}</td>
869
  <td>${it.goal ? (it.goal.type + ': ' + (it.goal.label || '')) : '-'}</td>
870
  <td>L${it.cascade_level || '-'} / ${it.operation || '-'}</td>
871
  <td>${baseline}</td>
 
929
  <div><strong>Шаг ${it.step}</strong> — ${safeHtml(it.status || '-')}</div>
930
  <span class="badge bg-secondary">${safeHtml(it.goal ? (it.goal.type + ': ' + (it.goal.label || '')) : '-')}</span>
931
  </div>
932
+ <div class="small mb-2"><strong>Этап:</strong> ${safeHtml(it.stage || (it.goal ? it.goal.type : '-') || '-')}</div>
933
  <div class="small mb-2"><strong>Каскад:</strong> L${safeHtml(it.cascade_level || '-')} / ${safeHtml(it.operation || '-')}</div>
934
  <div class="small mb-2"><strong>Причина:</strong> ${safeHtml(it.reason || '-')}</div>
935
  ${it.escalated_to_level ? `<div class="small mb-2 text-warning"><strong>Эскалация:</strong> переход на L${safeHtml(it.escalated_to_level)}</div>` : ''}
936
+ ${it.advanced_to_stage ? `<div class="small mb-2 text-info"><strong>Смена этапа:</strong> переход на ${safeHtml(it.advanced_to_stage)}</div>` : ''}
937
  <div class="small mb-2"><strong>Исходное предложение:</strong><br><span class="text-muted">${sentBefore}</span></div>
938
  <div class="small mb-2"><strong>Выбранный вариант:</strong><br><span class="text-muted">${sentAfterChosen}</span></div>
939
  <div class="table-responsive">
 
957
  <div class="small mb-2">
958
  Режим: <strong>${data.optimization_mode || 'balanced'}</strong>
959
  · Phrase Strategy: <strong>${data.phrase_strategy_mode || 'auto'}</strong>
960
+ · BERT target A-stage: <strong>${data.bert_stage_target ?? 0.7}</strong>
961
  </div>
962
  <div class="table-responsive">
963
  <table class="table table-sm table-bordered mb-0">
 
970
  <h6 class="card-title">Лог итераций</h6>
971
  <div class="table-responsive">
972
  <table class="table table-sm table-hover mb-0">
973
+ <thead><tr><th>#</th><th>Статус</th><th>Этап</th><th>Цель</th><th>Каскад</th><th>Baseline шага</th><th>Score до</th><th>Score после</th><th>Δ</th><th>Причина/комментарий</th></tr></thead>
974
+ <tbody>${iterRows || '<tr><td colspan="10" class="text-muted text-center">Нет данных</td></tr>'}</tbody>
975
  </table>
976
  </div>
977
  </div>
 
1015
  max_iterations: Number(document.getElementById('optimizerIterations').value || 2),
1016
  candidates_per_iteration: Number(document.getElementById('optimizerCandidates').value || 2),
1017
  temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
1018
+ bert_stage_target: Number(document.getElementById('optimizerBertStageTarget').value || 0.70),
1019
  optimization_mode: document.getElementById('optimizerMode').value || 'balanced',
1020
  phrase_strategy_mode: document.getElementById('optimizerPhraseStrategy').value || 'auto'
1021
  };