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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user