Skip to content

Commit cea7808

Browse files
authored
Merge pull request #31 from ronibhakta1/fix/changes-to-service-and-demo
refactor: update voice response model and improve demo interface
2 parents 746a3c9 + 210ea12 commit cea7808

13 files changed

Lines changed: 114 additions & 134 deletions

File tree

app/api/routes/voices.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010
@router.get(
1111
"/voices",
1212
response_model=list[Voice],
13-
response_model_exclude_none=True,
13+
response_model_exclude_defaults=True,
1414
dependencies=[Depends(require_ready)],
1515
summary="List available TTS voices",
1616
description=(
1717
"The voices **actually installed** on this deployment (realtime) — each voice's "
1818
"`language` and `otherLanguages` reflect what's loaded now, bounded by `LANGUAGES` + "
19-
"`VOICE_LANGUAGES`. Model-level `quality`/`controls` are merged in per voice; `controls` "
20-
"lists only the enabled ones. Optionally filtered by language or provider; supports "
19+
"`VOICE_LANGUAGES`. Fields left at their default (e.g. no `gender`, no cross-language "
20+
"`otherLanguages`) are omitted from the response. `quality` is the provider's default "
21+
"merged into each voice; `controls` is provider-wide only and lives on `GET /service`, "
22+
"not here. Optionally filtered by language or provider; supports "
2123
"pagination via `offset` and `limit`. Response headers `X-Total-Count`, `X-Offset`, "
2224
"`X-Limit` reflect the full result set size."
2325
),
@@ -40,15 +42,8 @@
4042
"provider": "pocket",
4143
"identifier": "urn:readium:tts:pocket:alba",
4244
"language": "en-US",
43-
"otherLanguages": [],
4445
"gender": "male",
4546
"quality": "veryHigh",
46-
"controls": {
47-
"pitch": False,
48-
"speed": False,
49-
"ssml": False,
50-
"boundary": False,
51-
},
5247
}
5348
],
5449
}

app/core/synthesizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def synthesize(
6666
)
6767

6868
provider, voice = self._catalog.resolve(voice_ref)
69-
boundaries_supported = voice.controls.boundary
69+
boundaries_supported = provider.default_controls.boundary
7070

7171
out = request.output
7272
logger.info(

app/providers/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ class TTSProvider(ABC):
1616
# Empty (default) = language-agnostic; list_voices() returns all voices unfiltered.
1717
supported_languages: ClassVar[frozenset[str]] = frozenset()
1818

19-
# Model-level defaults merged into every voice this provider serves, unless a
20-
# voice overrides them (see app/providers/voice_loading.py).
19+
# Model-level defaults. default_quality is merged into every voice this
20+
# provider serves (see app/providers/voice_loading.py) and also surfaced
21+
# server-wide via GET /service; default_controls is service-wide only.
2122
default_quality: ClassVar[Quality | None] = None
2223
default_controls: ClassVar[Controls] = Controls()
2324

app/providers/elevenlabs.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def _alignment_to_marks(
152152

153153
class ElevenLabsProvider(TTSProvider):
154154
id = "elevenlabs"
155-
default_quality: ClassVar[Quality] = Quality.HIGH
155+
default_quality: ClassVar[Quality] = Quality.VERY_HIGH
156156
# v2 has no pitch and only partial SSML; speed maps to voice_settings.speed,
157157
# boundary comes from the /with-timestamps alignment.
158158
default_controls: ClassVar[Controls] = Controls(
@@ -214,9 +214,7 @@ async def load(self) -> None:
214214
continue
215215
default_lang, installed = plan
216216
other_langs = installed - frozenset({primary})
217-
voice = build_voice(
218-
entry, self.id, other_langs, self.default_quality, self.default_controls
219-
)
217+
voice = build_voice(entry, self.id, other_langs, self.default_quality)
220218
self._voices.append(voice)
221219
self._voice_default_lang[voice.identifier] = default_lang
222220
self._voice_langs[voice.identifier] = installed

app/providers/pocket_tts.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,7 @@ def _load_sync(self) -> bool:
124124
continue
125125
default_lang, installed = plan
126126
other_langs = installed - frozenset({primary})
127-
voice = build_voice(
128-
entry, self.id, other_langs, self.default_quality, self.default_controls
129-
)
127+
voice = build_voice(entry, self.id, other_langs, self.default_quality)
130128
self._voices.append(voice)
131129
self._voice_default_lang[voice.identifier] = default_lang
132130
self._voice_langs[voice.identifier] = installed

app/providers/voice_loading.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
from pydantic import BaseModel
22

33
from app.domain.enums import Gender, Quality
4-
from app.schemas.voice import Controls, Voice
5-
6-
7-
class ControlsOverride(BaseModel):
8-
"""Partial per-voice override of a provider's default Controls. Unset (None)
9-
fields fall back to the provider default — see merge_controls()."""
10-
11-
pitch: bool | None = None
12-
speed: bool | None = None
13-
ssml: bool | None = None
14-
boundary: bool | None = None
4+
from app.schemas.voice import Voice
155

166

177
class VoiceEntry(BaseModel):
@@ -25,7 +15,6 @@ class VoiceEntry(BaseModel):
2515
otherLanguages: list[str] = []
2616
gender: Gender | None = None
2717
quality: Quality | None = None
28-
controls: ControlsOverride | None = None
2918

3019

3120
def _lang_prefix(lang: str) -> str:
@@ -84,23 +73,11 @@ def plan_install(
8473
return default_lang, installed
8574

8675

87-
def merge_controls(default: Controls, override: ControlsOverride | None) -> Controls:
88-
if override is None:
89-
return default
90-
return Controls(
91-
pitch=override.pitch if override.pitch is not None else default.pitch,
92-
speed=override.speed if override.speed is not None else default.speed,
93-
ssml=override.ssml if override.ssml is not None else default.ssml,
94-
boundary=override.boundary if override.boundary is not None else default.boundary,
95-
)
96-
97-
9876
def build_voice(
9977
entry: VoiceEntry,
10078
provider_id: str,
10179
installed_other: frozenset[str],
10280
default_quality: Quality | None,
103-
default_controls: Controls,
10481
) -> Voice:
10582
"""The merge point where 'possible' (voices.json) becomes 'installed'
10683
(served via /voices): otherLanguages is installed_other, not entry.otherLanguages."""
@@ -113,5 +90,4 @@ def build_voice(
11390
otherLanguages=sorted(installed_other),
11491
gender=entry.gender,
11592
quality=entry.quality or default_quality,
116-
controls=merge_controls(default_controls, entry.controls),
11793
)

app/schemas/voice.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
1-
from pydantic import BaseModel, model_serializer
1+
from pydantic import BaseModel
22

33
from app.domain.enums import Gender, Quality
44

55

66
class Controls(BaseModel):
7-
"""Which prosody/format controls a voice accepts. Provider-level defaults,
8-
overridable per voice (see app/providers/voice_loading.py).
9-
10-
Serializes only the controls that are ENABLED — a control the voice doesn't
11-
support is simply absent, not `false`. Keeps `/voices` and `/service` lean and
12-
works the same for any provider (pocket → `{}`, an SSML voice → `{"ssml": true}`).
13-
Internal Python access (e.g. `voice.controls.boundary`) still sees all fields."""
7+
"""Which prosody/format controls a provider supports. Server-wide per provider —
8+
see GET /service — not per voice."""
149

1510
pitch: bool = False
1611
speed: bool = False
1712
ssml: bool = False
1813
boundary: bool = False # true when the provider returns word-level timing marks
1914

20-
@model_serializer
21-
def _serialize_enabled_only(self) -> dict[str, bool]:
22-
return {k: True for k, v in self.__dict__.items() if v}
23-
2415
def as_dict(self) -> dict[str, bool]:
2516
"""Full booleans, including disabled ones — unlike the enabled-only JSON
2617
serialization above. Used by GET /service to show what a provider CAN do,
@@ -46,7 +37,6 @@ class Voice(BaseModel):
4637

4738
# --- server extensions (not in ReadiumSpeechVoice) ---
4839
provider: str
49-
controls: Controls = Controls()
5040

5141

5242
def voice_language_prefixes(voice: Voice) -> frozenset[str]:

0 commit comments

Comments
 (0)