From b4d4afd83ae573b21ed20f8f5e05e25af057a98b Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Sun, 5 Jan 2025 14:09:49 +0000 Subject: [PATCH 1/2] ENCOA-305 --- ielts_be/dtos/listening.py | 2 +- .../services/impl/exam/listening/__init__.py | 77 +++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/ielts_be/dtos/listening.py b/ielts_be/dtos/listening.py index d0a62da..09dbefa 100644 --- a/ielts_be/dtos/listening.py +++ b/ielts_be/dtos/listening.py @@ -30,7 +30,7 @@ class ConversationPayload(BaseModel): name: str gender: str text: str - voice: str + voice: Optional[str] = None class Dialog(BaseModel): conversation: Optional[List[ConversationPayload]] = Field(default_factory=list) diff --git a/ielts_be/services/impl/exam/listening/__init__.py b/ielts_be/services/impl/exam/listening/__init__.py index a2a81f0..a9a3345 100644 --- a/ielts_be/services/impl/exam/listening/__init__.py +++ b/ielts_be/services/impl/exam/listening/__init__.py @@ -1,7 +1,7 @@ import asyncio from logging import getLogger import random -from typing import Dict, Any +from typing import Dict, Any, Union from starlette.datastructures import UploadFile @@ -111,6 +111,15 @@ class ListeningService(IListeningService): return dialog async def generate_mp3(self, dto: Dialog) -> bytes: + convo = dto.conversation + voices_assigned = True + for segment in convo: + if segment.voice is None: + voices_assigned = False + + if not voices_assigned: + dto = self._get_conversation_voices(dto, True) + return await self._tts.text_to_speech(dto) async def create_instructions(self, text: str) -> bytes: @@ -263,7 +272,13 @@ class ListeningService(IListeningService): ) return {"dialog": response["monologue"]} - def _get_conversation_voices(self, response: Dict, unique_voices_across_segments: bool): + # TODO: This was a refactor from the previous ielts-be, don't know why there is a distinction between + # section 1 and 3, I think it would make sense to only keep only the section 1 logic, only bringing this up since + # there would need to be a refactor of the POST /api/listening/media endpoint which imo is pointless + # https://bitbucket.org/ecropdev/ielts-be/src/676f660f3e80220e3db0418dbeef0b1c0f257edb/helper/exercises.py?at=release%2Fmongodb-migration + """ + def generate_listening_1_conversation(topic: str): + ... chosen_voices = [] name_to_voice = {} for segment in response['conversation']: @@ -273,18 +288,70 @@ class ListeningService(IListeningService): voice = name_to_voice[name] else: voice = None + while voice is None: + if segment['gender'].lower() == 'male': + available_voices = MALE_NEURAL_VOICES + else: + available_voices = FEMALE_NEURAL_VOICES + + chosen_voice = random.choice(available_voices)['Id'] + if chosen_voice not in chosen_voices: + voice = chosen_voice + chosen_voices.append(voice) + name_to_voice[name] = voice + segment['voice'] = voice + return response + + + def generate_listening_3_conversation(topic: str): + ... + name_to_voice = {} + for segment in response['conversation']: + if 'voice' not in segment: + name = segment['name'] + if name in name_to_voice: + voice = name_to_voice[name] + else: + if segment['gender'].lower() == 'male': + voice = random.choice(MALE_NEURAL_VOICES)['Id'] + else: + voice = random.choice(FEMALE_NEURAL_VOICES)['Id'] + name_to_voice[name] = voice + segment['voice'] = voice + return response + """ + def _get_conversation_voices(self, response: Union[Dict, Dialog], unique_voices_across_segments: bool): + chosen_voices = [] + name_to_voice = {} + + is_model = isinstance(response, Dialog) + conversation = response.conversation if is_model else response['conversation'] + + for segment in conversation: + voice_check = (segment.voice is None) if is_model else ('voice' not in segment) + if voice_check: + name = segment.name if is_model else segment['name'] + if name in name_to_voice: + voice = name_to_voice[name] + else: + voice = None + gender = segment.gender if is_model else segment['gender'] # section 1 if unique_voices_across_segments: while voice is None: - chosen_voice = self._get_random_voice(segment['gender']) + chosen_voice = self._get_random_voice(gender) if chosen_voice not in chosen_voices: voice = chosen_voice chosen_voices.append(voice) # section 3 else: - voice = self._get_random_voice(segment['gender']) + voice = self._get_random_voice(gender) name_to_voice[name] = voice - segment['voice'] = voice + + if is_model: + segment.voice = voice + else: + segment['voice'] = voice return response @staticmethod From fb73213d631450f2f2fe9417a1baa796ea75dfa3 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Sun, 5 Jan 2025 19:04:23 +0000 Subject: [PATCH 2/2] ENCOA-308 --- firebase-debug.log | 13 +++++++++++++ ielts_be/dtos/user_batch.py | 2 +- ielts_be/services/impl/user.py | 6 +++--- tmp/110e7ab1-bba5-4768-8775-5e7a37f38a12.csv | 1 + 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 firebase-debug.log create mode 100644 tmp/110e7ab1-bba5-4768-8775-5e7a37f38a12.csv diff --git a/firebase-debug.log b/firebase-debug.log new file mode 100644 index 0000000..4c07155 --- /dev/null +++ b/firebase-debug.log @@ -0,0 +1,13 @@ +[debug] [2025-01-05T18:01:58.255Z] ---------------------------------------------------------------------- +[debug] [2025-01-05T18:01:58.257Z] Command: /usr/bin/node /usr/local/bin/firebase login --reauth +[debug] [2025-01-05T18:01:58.257Z] CLI Version: 13.28.0 +[debug] [2025-01-05T18:01:58.257Z] Platform: linux +[debug] [2025-01-05T18:01:58.257Z] Node Version: v18.19.1 +[debug] [2025-01-05T18:01:58.258Z] Time: Sun Jan 05 2025 18:01:58 GMT+0000 (Western European Standard Time) +[debug] [2025-01-05T18:01:58.258Z] ---------------------------------------------------------------------- +[debug] +[info] +[info] Visit this URL on this device to log in: +[info] https://accounts.google.com/o/oauth2/auth?client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&scope=email%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudplatformprojects.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ffirebase%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&response_type=code&state=211115440&redirect_uri=http%3A%2F%2Flocalhost%3A9005&login_hint=carlos.mesquita%40ecrop.dev +[info] +[info] Waiting for authentication... diff --git a/ielts_be/dtos/user_batch.py b/ielts_be/dtos/user_batch.py index 2198f5f..c5cdb06 100644 --- a/ielts_be/dtos/user_batch.py +++ b/ielts_be/dtos/user_batch.py @@ -15,7 +15,7 @@ class Entity(BaseModel): class UserDTO(BaseModel): - id: uuid.UUID = Field(default_factory=uuid.uuid4) + id: str email: str name: str type: str diff --git a/ielts_be/services/impl/user.py b/ielts_be/services/impl/user.py index b150876..deb6fd0 100644 --- a/ielts_be/services/impl/user.py +++ b/ielts_be/services/impl/user.py @@ -45,7 +45,6 @@ class UserService(IUserService): error_msg = f"Couldn't upload users. Failed to run command firebase auth import -> ```cmd {result.stdout}```" self._logger.error(error_msg) return error_msg - await self._init_users(batch_dto) FileHelper.remove_file(path) @@ -68,7 +67,7 @@ class UserService(IUserService): for user in batch_dto.users: user_data = { - 'UID': str(user.id), + 'UID': user.id, 'Email': user.email, 'Email Verified': False, 'Password Hash': user.passwordHash, @@ -142,7 +141,7 @@ class UserService(IUserService): 'subscriptionExpirationDate': user.expiryDate, 'entities': user.entities } - await self._db.save_to_db("users", new_user, str(user.id)) + await self._db.save_to_db("users", new_user, user.id) async def _create_code(self, user: UserDTO, maker_id: str) -> str: code = shortuuid.ShortUUID().random(length=6) @@ -174,6 +173,7 @@ class UserService(IUserService): 'name': user.groupName.strip(), 'participants': [user_id], 'disableEditing': False, + 'entity': user.entities[0]['id'] } await self._db.save_to_db("groups", new_group, str(uuid.uuid4())) else: diff --git a/tmp/110e7ab1-bba5-4768-8775-5e7a37f38a12.csv b/tmp/110e7ab1-bba5-4768-8775-5e7a37f38a12.csv new file mode 100644 index 0000000..164d72e --- /dev/null +++ b/tmp/110e7ab1-bba5-4768-8775-5e7a37f38a12.csv @@ -0,0 +1 @@ +8e44d32a-b921-4650-a0a5-2ad773356b61,batchentitytest1@ecrop.dev,False,5eoALGIXxSmwj8wRc3U7RGFM4tROrWs/+qJv6O9puXKoCCiniWlDQeWnCFXG8RJBrD48Hoqqoojso6rg31bTXA==,s76hIKNUacTPo8JqYx+7NA==,,,,,,,,,,,,,,,,,,,1736100072082,,