Files
VoiceStudio/tests/test_translate_degraded_not_failed.py
46141c8e5e fix(dub): a rate-limited polish pass no longer skips fitting, fails the UI, or ignores Retry-After (#1135)
* fix(dub): a rate-limited polish pass no longer skips fitting, fails the UI, or ignores Retry-After

Observed live (owner's Bengali dub, 4 segments): every cinematic reflect call
429'd against a free-tier OpenRouter model and the UI declared "4/4 segment(s)
failed" over a translate that succeeded. Root-causing that surfaced a class,
not a message bug:

The cinematic reflect/adapt chain is OPTIONAL polish — on any failure the
segment keeps its literal translation and is fully usable. But every such
degradation (no-llm, reflect/adapt errors, adapt-diverged, wrong-script,
cinematic-budget) was reported under the same "error" key as real translation
failures. Three consumers took that at face value:

  1. useDubWorkflow counted the rows as failed -> the red N/N toast;
  2. _stamp_predicted_rate_ratio and _stamp_duration_plan skipped them ->
     no rate badges, no fits/tight/impossible verdicts;
  3. _apply_fit_pass and the condense pass skipped them -> overlong lines went
     to synthesis unfitted and came out audibly time-compressed at mix. This
     is a direct contributor to "later segments got worse" in rate-limited
     Cinematic dubs.

Split the vocabulary: "error" now means the row has no usable text (base
translation failed); optional-pass fallbacks ride a separate "degraded" key.
Downstream filters keep gating on "error" only, so degraded rows flow through
every fitting pass. The UI shows an amber "translated, polish skipped
(<reason>)" toast and a mild row tooltip instead of a red failure, and editing
a row clears the stale annotation.

And the retry that makes most of this moot: _chat now honors a 429's
Retry-After once (capped at 30s, jittered so the 6-wide segment fan-out does
not re-stampede the same window). OpenRouter's free pool says "Retry-After: 2"
- giving up instantly turned a two-second wait into a whole failed pass.

Tests: producer contract (every cinematic fallback returns degraded, never
error - 5 updated + retained), consumer contract (degraded rows still get
rate-ratio prediction and duration plans; error rows stay excluded), and the
retry (honors small Retry-After with jitter, caps absurd ones, one retry only,
non-429s never retry). Full suite: 2981 backend + 1236 frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): correct PR ref to #1135

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dub): review round — localize the degraded strings, un-suppress the mixed toast, clear stale annotations on edit

Three review findings, all valid:

- Localization parity (Greptile): the two new user-facing keys existed only in
  en.json. Every other key in these namespaces is translated in all 21
  locales, so the fallback-to-English behavior would have been a regression of
  the repo's parity convention. Both keys now translated in all 20 non-en
  locales, inserted beside their siblings.
- Mixed responses suppressed the degraded story (Greptile): when a translate
  returned both real failures and degraded rows, only the red failure toast
  fired. The degraded warning now fires alongside it — real failures don't
  erase what happened to the rows that succeeded plainly.
- Ordinary edits kept stale annotations (CodeRabbit): the restore path cleared
  translate_error/translate_degraded but a normal text edit didn't, so a row
  kept wearing "polish pass skipped" over words the user had just written.
  Editing the text now clears both annotations.

Frontend suite: 1236 passed; i18n probe green across all 21 locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 16:13:41 +05:30

66 lines
2.6 KiB
Python

"""Degraded ≠ failed: a skipped polish pass must not disable downstream passes.
The cinematic reflect/adapt chain is optional polish — on any failure (rate
limit, budget, divergent reply, no LLM) the segment keeps its literal
translation and is fully usable. Those degradations used to be reported under
the same ``error`` key as real translation failures, which had three
consequences, each pinned here or in test_translator.py:
1. the UI toasted "N/N segment(s) failed" over a translate that succeeded
(frontend counts ``error`` rows);
2. the speech-rate / rate-ratio prediction skipped the row;
3. duration planning and the fit pass skipped the row — overlong lines then
hit heavy time-compression at generation, audibly degrading the dub
(observed live: 4/4 reflect 429s → no fit pass → compressed segments).
``error`` now means "no usable text" (base translation failed); optional-pass
fallbacks ride a separate ``degraded`` key.
"""
from __future__ import annotations
from schemas.requests import TranslateRequest, TranslateSegment
def _req(n=2, slot=3.0):
return TranslateRequest(
segments=[
TranslateSegment(id=str(i), text=f"line {i}", slot_seconds=slot)
for i in range(1, n + 1)
],
target_lang="bn",
)
def test_degraded_rows_still_get_rate_ratio_prediction():
from api.routers.dub_translate import _stamp_predicted_rate_ratio
rows = [
{"id": "1", "text": "একটি অনুবাদিত লাইন", "degraded": "reflect: 429"},
{"id": "2", "text": "another line", "error": "llm-failed"},
]
_stamp_predicted_rate_ratio(rows, _req())
assert "rate_ratio" in rows[0], (
"a degraded row (usable literal text) was excluded from rate-ratio "
"prediction — degraded is being treated as failed again"
)
assert "rate_ratio" not in rows[1] # real failures stay excluded
def test_degraded_rows_still_get_duration_plan():
from api.routers.dub_translate import _stamp_duration_plan
req = TranslateRequest(
segments=[
TranslateSegment(id="1", text="a", slot_seconds=3.0, start=0.0, end=3.0),
TranslateSegment(id="2", text="b", slot_seconds=3.0, start=3.5, end=6.5),
],
target_lang="bn",
)
rows = [
{"id": "1", "text": "একটি অনুবাদিত লাইন", "degraded": "cinematic-budget"},
{"id": "2", "text": "another line", "error": "llm-failed"},
]
_stamp_duration_plan(rows, req)
assert "plan" in rows[0], "degraded row skipped by the duration planner"
assert "plan" not in rows[1]