fix(translate): fix batch sizing — use real available input budget, respect job.batch_size, lazy playwright in docker

- _batch_sizer.py: compute per_batch_budget from actual available_input_budget
  (context_window - max_output_tokens) instead of sum of first N rows
- _batch_sizer.py: cap max_rows_hard_cap with job.batch_size (user config)
- _token_budget.py: add available_input_budget and max_output_tokens to return
- preview.py: validate required config fields before preview
- requirements-docker.txt: add playwright pip package (~5 MB)
- backend.entrypoint.sh: lazy playwright install chromium on first start
- build.sh: switch tar to tar.xz (-T0 -9), 5.5x smaller bundles
- README.md: add offline bundle deployment instructions
- playwright.config.js: add testMatch for *.e2e.js files
- frontend: preview tab config validation, i18n keys, translationRun store
This commit is contained in:
2026-05-17 23:32:00 +03:00
parent 6988e63967
commit 9228d071ef
15 changed files with 195 additions and 46 deletions

View File

@@ -20,7 +20,14 @@ from ...core.config_manager import ConfigManager
from ...core.logger import belief_scope, logger
from ...services.llm_provider import LLMProviderService
from ...models.translate import TranslationJob
from ._token_budget import PROMPT_BASE_TOKENS, estimate_token_budget
from ._token_budget import (
JSON_OVERHEAD_PER_ROW,
MAX_OUTPUT_HEADROOM,
OUTPUT_PER_ROW_PER_LANG,
PROMPT_BASE_TOKENS,
REASONING_OVERHEAD,
estimate_token_budget,
)
from ._utils import estimate_row_tokens
@@ -115,15 +122,24 @@ class AdaptiveBatchSizer:
]
# 3. Compute per-batch row-content budget
estimated_input = budget.get("estimated_input_tokens", 50000)
per_batch_budget = estimated_input - PROMPT_BASE_TOKENS
# Use the ACTUAL available input capacity (context_window - max_output_tokens),
# NOT the sum of the first N rows. This prevents long rows from being
# incorrectly placed in 1-row batches when the context window has plenty of room.
available_input = budget.get("available_input_budget")
if available_input is not None:
# New-style: use actual available input capacity
per_batch_budget = available_input - PROMPT_BASE_TOKENS
else:
# Fallback for tests: use estimated_input (sum of first N rows)
estimated_input = budget.get("estimated_input_tokens", 50000)
per_batch_budget = estimated_input - PROMPT_BASE_TOKENS
if per_batch_budget <= 0:
fallback_size = job.batch_size or 50
logger.explore("Per-batch budget collapsed — falling back to fixed batch size", {
"estimated_input": estimated_input,
"prompt_base": PROMPT_BASE_TOKENS,
"per_batch_budget": per_batch_budget,
"fallback_size": fallback_size,
"total_rows": len(source_rows),
})
return [
source_rows[i:i + fallback_size]
@@ -131,7 +147,26 @@ class AdaptiveBatchSizer:
]
# 4. Greedy batch splitting
max_rows_hard_cap = max(recommended * 2, 20)
# Compute max rows per batch from OUTPUT constraint.
# This prevents output truncation (finish_reason=length) when batching
# many short rows within the large input budget.
num_languages = len(target_languages)
max_output_tokens_val = budget.get("max_output_tokens")
if max_output_tokens_val is not None:
output_per_row = num_languages * OUTPUT_PER_ROW_PER_LANG + JSON_OVERHEAD_PER_ROW
available_output = max_output_tokens_val - REASONING_OVERHEAD - MAX_OUTPUT_HEADROOM
max_rows_by_output = max(available_output // output_per_row, 1) if output_per_row > 0 else 20
max_rows_hard_cap = max_rows_by_output
else:
# Fallback for tests: old formula
max_rows_hard_cap = max(recommended * 2, 20)
# 5. Respect job.batch_size as the absolute maximum rows per batch.
# User-configured batch_size overrides model-based estimates to
# prevent LLM quality degradation on large batches.
if job.batch_size:
max_rows_hard_cap = min(max_rows_hard_cap, job.batch_size)
batches: list[list[dict]] = []
current_batch: list[dict] = []
current_tokens = 0

View File

@@ -235,6 +235,8 @@ def estimate_token_budget(
estimated_output_tokens: Total estimated output tokens.
max_output_needed: Recommended max_tokens for this batch.
warning: str | None if batch was reduced.
available_input_budget: Total input token budget per batch (context_window - max_output_tokens).
max_output_tokens: The effective max output tokens limit used for this batch.
"""
if not target_languages:
target_languages = ["en"]
@@ -333,6 +335,8 @@ def estimate_token_budget(
"estimated_output_tokens": estimated_output,
"max_output_needed": max_output_needed,
"warning": warning,
"available_input_budget": available_input_budget,
"max_output_tokens": max_output_tokens,
}
# endregion estimate_token_budget
# #endregion estimate_token_budget

View File

@@ -80,6 +80,10 @@ class TranslationPreview:
raise ValueError("Job must have a source datasource configured for preview")
if not job.translation_column:
raise ValueError("Job must have a translation column configured for preview")
if not job.target_languages:
raise ValueError("Job must have at least one target language configured for preview")
if not job.provider_id:
raise ValueError("Job must have an LLM provider configured for preview")
target_languages = job.target_languages or [job.target_dialect or "en"]
if not isinstance(target_languages, list):