Skip to content

Commit 60c5e86

Browse files
committed
List all available voices by pagination
1 parent c668455 commit 60c5e86

2 files changed

Lines changed: 98 additions & 7 deletions

File tree

src/esperanto/providers/tts/mistral.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,34 @@ def _get_models(self) -> List[Model]:
6666
def available_voices(self) -> Dict[str, Voice]:
6767
if self._voices_cache is not None:
6868
return self._voices_cache
69-
items = []
70-
page = 1
69+
70+
items: List[Dict[str, Any]] = []
71+
offset = 0
72+
limit = 100
7173
while True:
7274
response = self.client.get(
7375
f"{self.base_url}/audio/voices",
7476
headers=self._get_headers(),
75-
params={"page": page},
77+
params={"limit": limit, "offset": offset},
7678
)
7779
self._handle_error(response)
7880
body = response.json()
79-
items.extend(body.get("items", []))
80-
if page >= body.get("total_pages", 1):
81+
page_items = body.get("items", [])
82+
items.extend(page_items)
83+
84+
total = body.get("total")
85+
if total is not None:
86+
if len(items) >= total:
87+
break
88+
elif len(page_items) < limit:
89+
break
90+
91+
if not page_items:
92+
# To avoid an infinite loop.
8193
break
82-
page += 1
94+
95+
offset += len(page_items)
96+
8397
self._voices_cache = {
8498
item["id"]: Voice(
8599
id=item["id"],

tests/providers/tts/test_mistral.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def test_available_voices(tts_model):
163163
tts_model.client.get.assert_called_once_with(
164164
"https://api.mistral.ai/v1/audio/voices",
165165
headers=tts_model._get_headers(),
166-
params={"page": 1},
166+
params={"limit": 100, "offset": 0},
167167
)
168168

169169
assert "gb_jane_neutral" in voices
@@ -180,6 +180,83 @@ def test_available_voices_cached(tts_model):
180180
assert tts_model.client.get.call_count == 1
181181

182182

183+
def test_available_voices_paginated(tts_model):
184+
"""Multi-page response: all pages' voices should be returned."""
185+
page_one_items = [
186+
{
187+
"id": f"voice_{i}",
188+
"name": f"Voice {i}",
189+
"gender": "NEUTRAL",
190+
"languages": ["en"],
191+
}
192+
for i in range(100)
193+
]
194+
page_two_items = [
195+
{
196+
"id": "voice_100",
197+
"name": "Voice 100",
198+
"gender": "NEUTRAL",
199+
"languages": ["en"],
200+
},
201+
{
202+
"id": "voice_101",
203+
"name": "Voice 101",
204+
"gender": "NEUTRAL",
205+
"languages": ["en"],
206+
},
207+
]
208+
209+
def make_response(status_code, json_data):
210+
response = Mock()
211+
response.status_code = status_code
212+
response.json.return_value = json_data
213+
response.text = ""
214+
return response
215+
216+
def paginated_get(url, **kwargs):
217+
params = kwargs.get("params", {})
218+
offset = params.get("offset", 0)
219+
if offset == 0:
220+
return make_response(
221+
200,
222+
{
223+
"items": page_one_items,
224+
"page": 1,
225+
"page_size": 100,
226+
"total": 102,
227+
"total_pages": 2,
228+
},
229+
)
230+
if offset == 100:
231+
return make_response(
232+
200,
233+
{
234+
"items": page_two_items,
235+
"page": 2,
236+
"page_size": 100,
237+
"total": 102,
238+
"total_pages": 2,
239+
},
240+
)
241+
return make_response(200, {"items": [], "total": 102})
242+
243+
tts_model.client.get.side_effect = paginated_get
244+
245+
voices = tts_model.available_voices
246+
247+
assert len(voices) == 102
248+
assert "voice_0" in voices
249+
assert "voice_99" in voices
250+
assert "voice_100" in voices
251+
assert "voice_101" in voices
252+
assert tts_model.client.get.call_count == 2
253+
254+
first_call = tts_model.client.get.call_args_list[0]
255+
second_call = tts_model.client.get.call_args_list[1]
256+
assert first_call.kwargs["params"] == {"limit": 100, "offset": 0}
257+
assert second_call.kwargs["params"] == {"limit": 100, "offset": 100}
258+
259+
183260
def test_error_handling_4xx(tts_model):
184261
tts_model.client.post.side_effect = None
185262
error_response = Mock()

0 commit comments

Comments
 (0)