Skip to content

Commit 5acd0e5

Browse files
Support speaker names in voice assignment
Allow voices to be assigned using speaker names/roles in addition to speaker order (e.g., speaker_1, speaker_2). Speaker names are case-insensitive, so "Doctor", "DOCTOR", and "doctor" are treated equivalently. Examples: ```python # Assigning voices using speaker names / roles dialog.to_audio( "path/to/output/audio", voices={ "Doctor": "path/to/doctor_voice_reference.wav", "Patient": "path/to/patient_voice_reference.wav", } ) # Assigning voices using speaker orders (speaker_1, speaker_2) dialog.to_audio( "path/to/output/audio", voices={ "speaker_1": "path/to/doctor_voice_reference.wav", "speaker_2": "path/to/patient_voice_reference.wav", } ) ``` Speaker names are case insensitive, so this will also work exactly the same as the first example: ```python dialog.to_audio( "path/to/output/audio", voices={ "DOCTOR": "path/to/doctor_voice_reference.wav", "PATIENT": "path/to/patient_voice_reference.wav", } ) ``` This keeps backward compatibility while making voice configuration more flexible and readable.
1 parent 16123a7 commit 5acd0e5

4 files changed

Lines changed: 38 additions & 12 deletions

File tree

src/sdialog/audio/dialog.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@
4646
from sdialog import Dialog
4747
from sdialog.audio.turn import AudioTurn
4848
from sdialog.audio.room import AudioSource
49-
from sdialog.audio.utils import logger, generate_reference_voices, Role
5049
from sdialog.audio.tts.base import BaseTTS, BaseVoiceCloneTTS
5150
from sdialog.audio.voice_database import BaseVoiceDatabase, Voice
51+
from sdialog.audio.utils import Role, CaseInsensitiveDict, logger, generate_reference_voices
5252

5353

5454
class AudioDialog(Dialog):
@@ -68,8 +68,8 @@ class AudioDialog(Dialog):
6868
audio_step_2_filepath: str = ""
6969
audio_step_3_filepaths: dict[str, dict] = {}
7070

71-
speakers_names: dict[str, str] = {}
72-
speakers_roles: dict[str, str] = {}
71+
speakers_names: dict[str, str] = {} # role2name mapping
72+
speakers_roles: dict[str, str] = CaseInsensitiveDict() # name2role mapping (case insensitive)
7373

7474
def __init__(self, **kwargs):
7575
super().__init__(**kwargs)
@@ -177,16 +177,14 @@ def from_dialog(dialog: Dialog):
177177
speakers.append(turn.speaker)
178178
if len(speakers) == 2:
179179
break
180-
speaker_1 = speakers[0]
181-
speaker_2 = speakers[1]
182180

183181
# Create role mappings for speaker identification
184-
audio_dialog.speakers_names[Role.SPEAKER_1] = speaker_1
185-
audio_dialog.speakers_names[Role.SPEAKER_2] = speaker_2
182+
audio_dialog.speakers_names[Role.SPEAKER_1] = speakers[0]
183+
audio_dialog.speakers_names[Role.SPEAKER_2] = speakers[1]
186184

187185
# Create reverse mappings for role lookup
188-
audio_dialog.speakers_roles[speaker_1] = Role.SPEAKER_1
189-
audio_dialog.speakers_roles[speaker_2] = Role.SPEAKER_2
186+
audio_dialog.speakers_roles[speakers[0]] = Role.SPEAKER_1
187+
audio_dialog.speakers_roles[speakers[1]] = Role.SPEAKER_2
190188

191189
return audio_dialog
192190

@@ -478,8 +476,8 @@ def persona_to_voice(
478476
# Get the role of the speaker (speaker_1 or speaker_2)
479477
role: Role = self.speakers_roles[speaker]
480478

481-
if voices is not None and voices != {} and role not in voices:
482-
raise ValueError(f"Voice for role {str(role)} not found in the voices dictionary")
479+
if voices and role not in voices and speaker not in voices:
480+
raise ValueError(f"Voice for {str(role)} not found in the voices dictionary")
483481

484482
# If no voices are provided, get a voice from the voice database based on the gender, age and language
485483
if voices is None or voices == {}:

src/sdialog/audio/pipeline.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,10 @@ def inference(
543543
if not verbose:
544544
logger.setLevel(logging.ERROR)
545545

546+
if voices is not None:
547+
voices = {dialog.speakers_roles[key] if key in dialog.speakers_roles else key: value
548+
for key, value in voices.items()}
549+
546550
# Reset the logger level to the original level after the function is executed
547551
try:
548552

src/sdialog/audio/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,30 @@ def __str__(self):
424424
return self.value
425425

426426

427+
class CaseInsensitiveDict(dict):
428+
def __setitem__(self, key, value):
429+
super().__setitem__(key.lower(), value)
430+
431+
def __getitem__(self, key):
432+
return super().__getitem__(key.lower())
433+
434+
def __delitem__(self, key):
435+
super().__delitem__(key.lower())
436+
437+
def __contains__(self, key):
438+
return super().__contains__(key.lower())
439+
440+
def get(self, key, default=None):
441+
return super().get(key.lower(), default)
442+
443+
def update(self, other=None, **kwargs):
444+
if other:
445+
for k, v in dict(other).items():
446+
self[k] = v
447+
for k, v in kwargs.items():
448+
self[k] = v
449+
450+
427451
def default_dscaper_datasets() -> list[str]:
428452
"""
429453
Default dSCAPER datasets

tests/test_audio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ def test_persona_to_voice_missing_role_in_voices_dict(dialog_with_personas):
693693
# SPEAKER_2 is missing
694694
}
695695

696-
with pytest.raises(ValueError, match="Voice for role speaker_2 not found in the voices dictionary"):
696+
with pytest.raises(ValueError):
697697
dialog_with_personas.persona_to_voice(mock_voice_db, voices=voices)
698698

699699

0 commit comments

Comments
 (0)