aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_tts_sglomni.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_tts_sglomni.py')
-rw-r--r--app/tests/test_tts_sglomni.py148
1 files changed, 146 insertions, 2 deletions
diff --git a/app/tests/test_tts_sglomni.py b/app/tests/test_tts_sglomni.py
index 85327e1..0e3a7de 100644
--- a/app/tests/test_tts_sglomni.py
+++ b/app/tests/test_tts_sglomni.py
@@ -191,6 +191,7 @@ class PayloadTests(unittest.TestCase):
client.instructions = kwargs.get("instructions", "")
client.language = "English"
client._seed = None
+ client._kv_fit = None
return client
def test_speaker_payload_sends_the_preset_name(self):
@@ -256,10 +257,19 @@ class PayloadTests(unittest.TestCase):
def test_higgs_payload_raises_the_generation_cap(self):
"""Higgs's 2048-frame engine default caps a request at ~27 s
- (75 fps), below a full 250-word sub-chunk."""
+ (75 fps); the catalog raises it to the most its admission window
+ allows (~40 s after the prompt tokens)."""
client = self._make_client("higgs_audio_v3_tts")
payload = client._request_payload("Hello.")
- self.assertEqual(payload["max_new_tokens"], 12288)
+ self.assertEqual(payload["max_new_tokens"], 3000)
+
+ def test_payload_keeps_a_learned_kv_fit(self):
+ """A capacity learned from an admission rejection caps later
+ requests below the catalog value."""
+ client = self._make_client("higgs_audio_v3_tts")
+ client._kv_fit = 2500
+ self.assertEqual(client._request_payload("Hello.")["max_new_tokens"],
+ 2500)
def test_models_without_a_cap_send_no_max_new_tokens(self):
client = self._make_client("moss_tts")
@@ -293,6 +303,126 @@ class RequestErrorTests(unittest.TestCase):
self.assertIsInstance(error, NonRetryableTTSError)
+_KV_REJECTION_BODY = json.dumps({"error": {
+ "message": "Request requires more tokens than the thinker KV cache "
+ "can hold (input_tokens=684, max_new_tokens=12288, "
+ "required_tokens=12972, kv_capacity=4095). Current "
+ "mem_fraction_static is 0.800; try setting "
+ "--thinker-mem-fraction-static higher.",
+ "type": "InternalServerError", "code": 500}})
+
+
+def _http_error(code: int, body: str) -> urllib.error.HTTPError:
+ return urllib.error.HTTPError(
+ "http://127.0.0.1:8100/v1/audio/speech", code, "error",
+ hdrs=None, fp=io.BytesIO(body.encode("utf-8")))
+
+
+def _speech_response():
+ response = MagicMock()
+ response.__enter__.return_value = response
+ response.read.return_value = _WAV_BYTES
+ return response
+
+
+class KvAdmissionTests(unittest.TestCase):
+ """The KV-window admission rejection refits max_new_tokens once."""
+
+ def _client(self):
+ client = SgOmniTTSClient.__new__(SgOmniTTSClient)
+ from backends.sglomni.catalog import entry_by_key
+ client.entry = entry_by_key("higgs_audio_v3_tts")
+ client.api_url = "http://127.0.0.1:8100"
+ client.voice = None
+ client.ref_audio = None
+ client.ref_text = ""
+ client.instructions = ""
+ client.language = "English"
+ client._seed = None
+ client.chunk_size = None
+ client._kv_fit = None
+ return client
+
+ def test_fit_is_parsed_from_the_server_message(self):
+ fit = self._client()._kv_admission_fit(_KV_REJECTION_BODY)
+ # kv_capacity 4095 - input 684 - the 64-frame margin.
+ self.assertEqual(fit, 3347)
+
+ def test_fit_is_cached_for_later_requests(self):
+ client = self._client()
+ client._kv_admission_fit(_KV_REJECTION_BODY)
+ client._kv_admission_fit(_KV_REJECTION_BODY)
+ self.assertEqual(client._kv_fit, 3347)
+
+ def test_unrelated_errors_do_not_fit(self):
+ client = self._client()
+ self.assertIsNone(client._kv_admission_fit("CUDA out of memory"))
+ self.assertIsNone(client._kv_fit)
+
+ def test_a_window_below_the_floor_raises_with_guidance(self):
+ body = json.dumps({"error": {"message":
+ "Request requires more tokens than the thinker KV cache can "
+ "hold (input_tokens=4000, max_new_tokens=12288, "
+ "required_tokens=16288, kv_capacity=4095).", "code": 500}})
+ with self.assertRaises(NonRetryableTTSError) as ctx:
+ self._client()._kv_admission_fit(body)
+ self.assertIn("shorter reference clip", str(ctx.exception))
+
+ def test_request_wav_refits_and_resends_once(self):
+ client = self._client()
+ with patch(
+ "converter.clients.sglomni.urllib.request.urlopen",
+ side_effect=[_http_error(500, _KV_REJECTION_BODY),
+ _speech_response()]) as mock_open:
+ wav = client._request_wav("Hello.")
+ self.assertEqual(wav, _WAV_BYTES)
+ self.assertEqual(mock_open.call_count, 2)
+ refit = json.loads(mock_open.call_args[0][0].data)
+ self.assertEqual(refit["max_new_tokens"], 3347)
+
+ def test_request_wav_surfaces_a_refit_that_fails_again(self):
+ client = self._client()
+ with patch(
+ "converter.clients.sglomni.urllib.request.urlopen",
+ side_effect=[_http_error(500, _KV_REJECTION_BODY),
+ _http_error(500, _KV_REJECTION_BODY)]):
+ with self.assertRaises(RuntimeError) as ctx:
+ client._request_wav("Hello.")
+ message = str(ctx.exception)
+ self.assertIn("HTTP 500", message)
+ self.assertIn("thinker KV cache", message)
+ self.assertNotIsInstance(ctx.exception, NonRetryableTTSError)
+
+ def test_request_wav_does_not_refit_other_errors(self):
+ client = self._client()
+ with patch(
+ "converter.clients.sglomni.urllib.request.urlopen",
+ side_effect=[_http_error(500, "CUDA out of memory")]):
+ with self.assertRaises(RuntimeError) as ctx:
+ client._request_wav("Hello.")
+ self.assertIn("CUDA out of memory", str(ctx.exception))
+ self.assertIsNone(client._kv_fit)
+
+ def test_request_wav_keeps_the_refit_across_sub_requests(self):
+ # A tight window (a long reference clip): the fit binds below the
+ # 3000-frame catalog cap, and every later request carries it.
+ tight_body = json.dumps({"error": {"message":
+ "Request requires more tokens than the thinker KV cache can "
+ "hold (input_tokens=1500, max_new_tokens=3000, "
+ "required_tokens=4500, kv_capacity=4095).", "code": 500}})
+ client = self._client()
+ with patch(
+ "converter.clients.sglomni.urllib.request.urlopen",
+ side_effect=[_http_error(500, tight_body),
+ _speech_response(),
+ _speech_response()]) as mock_open:
+ client._request_wav("Hello.")
+ client._request_wav("Hello again.")
+ self.assertEqual(mock_open.call_count, 3)
+ second = json.loads(mock_open.call_args[0][0].data)
+ self.assertEqual(second["max_new_tokens"], 2531)
+
+
class GenerateChunkTests(unittest.TestCase):
"""Chunk generation: WAV output, sub-chunking, bookkeeping."""
@@ -315,6 +445,8 @@ class GenerateChunkTests(unittest.TestCase):
client.instructions = ""
client.language = "English"
client._seed = None
+ client.chunk_size = None
+ client._kv_fit = None
return client
def _read_wav(self, path):
@@ -347,6 +479,18 @@ class GenerateChunkTests(unittest.TestCase):
self.assertEqual(len(args[0]), 3)
self.assertEqual(args[1], Path(result))
+ def test_run_chunk_size_caps_the_sub_requests(self):
+ """The pre-flight clamp (a chunk_words-capped model's popup
+ answer) overrides CHUNK_SIZE for this run."""
+ client = self._make_client()
+ client.chunk_size = 10
+ text = " ".join(f"word{i}" for i in range(24))
+ with patch.object(client, "_request_wav",
+ return_value=_WAV_BYTES) as mock_wav, \
+ patch("converter.clients.sglomni.concat_audio_files"):
+ client.generate_chunk(text, 1)
+ self.assertEqual(mock_wav.call_count, 3)
+
def test_single_subchunk_skips_concatenation(self):
client = self._make_client()
with patch.object(client, "_request_wav", return_value=_WAV_BYTES), \