diff --git a/ielts_be/helpers/__init__.py b/ielts_be/helpers/__init__.py index e628b0d..ebeaa3d 100644 --- a/ielts_be/helpers/__init__.py +++ b/ielts_be/helpers/__init__.py @@ -2,10 +2,12 @@ from .file import FileHelper from .text import TextHelper from .token_counter import count_tokens from .exercises import ExercisesHelper +from .difficulty import DifficultyHelper __all__ = [ "FileHelper", "TextHelper", "count_tokens", "ExercisesHelper", + "DifficultyHelper" ] diff --git a/ielts_be/helpers/difficulty.py b/ielts_be/helpers/difficulty.py new file mode 100644 index 0000000..4ae0552 --- /dev/null +++ b/ielts_be/helpers/difficulty.py @@ -0,0 +1,40 @@ +import math +import random +from typing import Optional, List, Iterator + +from ielts_be.configs.constants import EducationalContent + + +class DifficultyHelper: + + def __init__(self, difficulties: Optional[List[str]]): + self.difficulties = difficulties + self.distributed: Optional[Iterator[str]] = None + + def distribute_for_count(self, count: int) -> None: + if not self.difficulties or count == 0: + return + + result = [] + remaining = count + difficulties_count = len(self.difficulties) + + for i, diff in enumerate(self.difficulties): + if i == difficulties_count - 1: + slots = remaining + else: + slots = math.ceil(remaining / (difficulties_count - i)) + + result.extend([diff] * slots) + remaining -= slots + + self.distributed = iter(result) + + def pick_difficulty(self, difficulty: Optional[str]) -> str: + if difficulty: + return difficulty if difficulty != "Random" else random.choice(EducationalContent.DIFFICULTIES) + + if self.distributed: + return next(self.distributed) + + return random.choice(EducationalContent.DIFFICULTIES) diff --git a/ielts_be/services/impl/exam/level/__init__.py b/ielts_be/services/impl/exam/level/__init__.py index 5b48655..846a58d 100644 --- a/ielts_be/services/impl/exam/level/__init__.py +++ b/ielts_be/services/impl/exam/level/__init__.py @@ -8,12 +8,12 @@ import random from ielts_be.configs.constants import EducationalContent from ielts_be.dtos.level import LevelExercisesDTO +from ielts_be.helpers import DifficultyHelper from ielts_be.repositories import IDocumentStore from ielts_be.services import ( ILevelService, ILLMService, IReadingService, IWritingService, IListeningService, ISpeakingService ) -from ielts_be.utils import pick_difficulty from .exercises import MultipleChoice, BlankSpace, PassageUtas, FillBlanks from .full_exams import CustomLevelModule, LevelUtas from .upload import UploadLevelModule @@ -51,8 +51,7 @@ class LevelService(ILevelService): async def upload_level(self, upload: UploadFile, solutions: Optional[UploadFile] = None) -> Dict: return await self._upload_module.generate_level_from_file(upload, solutions) - async def _generate_exercise(self, req_exercise, start_id, difficulties): - difficulty = pick_difficulty(req_exercise.difficulty, difficulties) + async def _generate_exercise(self, req_exercise, start_id, difficulty): if req_exercise.type == "mcBlank": questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, difficulty, start_id) questions["variant"] = "mcBlank" @@ -95,16 +94,21 @@ class LevelService(ILevelService): return exercise async def generate_exercises(self, dto: LevelExercisesDTO): - start_ids = [] current_id = 1 + tasks = [] + + distributor = DifficultyHelper(dto.difficulty) + + none_count = sum(1 for ex in dto.exercises if ex.difficulty is None) + distributor.distribute_for_count(none_count) + for req_exercise in dto.exercises: - start_ids.append(current_id) + difficulty = distributor.pick_difficulty(req_exercise.difficulty) + tasks.append( + self._generate_exercise(req_exercise, current_id, difficulty) + ) current_id += req_exercise.quantity - tasks = [ - self._generate_exercise(req_exercise, start_id, dto.difficulty) - for req_exercise, start_id in zip(dto.exercises, start_ids) - ] questions = await gather(*tasks) questions = [{'id': str(uuid4()), **exercise} for exercise in questions] diff --git a/ielts_be/services/impl/exam/listening/__init__.py b/ielts_be/services/impl/exam/listening/__init__.py index 81465ad..f874f20 100644 --- a/ielts_be/services/impl/exam/listening/__init__.py +++ b/ielts_be/services/impl/exam/listening/__init__.py @@ -13,15 +13,13 @@ from ielts_be.configs.constants import ( NeuralVoices, GPTModels, TemperatureSettings, EducationalContent, FieldsAndExercises ) -from ielts_be.helpers import FileHelper +from ielts_be.helpers import FileHelper, DifficultyHelper from .audio_to_dialog import AudioToDialog from .import_listening import ImportListeningModule from .write_blank_forms import WriteBlankForms from .write_blanks import WriteBlanks from .write_blank_notes import WriteBlankNotes from ..shared import TrueFalse, MultipleChoice -from ielts_be.utils import pick_difficulty - class ListeningService(IListeningService): @@ -137,6 +135,11 @@ class ListeningService(IListeningService): start_id = 1 exercise_tasks = [] + diff_helper = DifficultyHelper(dto.difficulty) + + none_count = sum(1 for ex in dto.exercises if ex.difficulty is None) + diff_helper.distribute_for_count(none_count) + for req_exercise in dto.exercises: exercise_tasks.append( self._generate_exercise( @@ -144,7 +147,7 @@ class ListeningService(IListeningService): "dialog or monologue", dto.text, start_id, - pick_difficulty(req_exercise.difficulty, dto.difficulty) + diff_helper.pick_difficulty(req_exercise.difficulty) ) ) start_id += req_exercise.quantity diff --git a/ielts_be/services/impl/exam/reading/__init__.py b/ielts_be/services/impl/exam/reading/__init__.py index 12174bd..7f1e662 100644 --- a/ielts_be/services/impl/exam/reading/__init__.py +++ b/ielts_be/services/impl/exam/reading/__init__.py @@ -5,9 +5,8 @@ from fastapi import UploadFile from ielts_be.configs.constants import GPTModels, FieldsAndExercises, TemperatureSettings from ielts_be.dtos.reading import ReadingDTO -from ielts_be.helpers import ExercisesHelper +from ielts_be.helpers import ExercisesHelper, DifficultyHelper from ielts_be.services import IReadingService, ILLMService -from ielts_be.utils import pick_difficulty from .fill_blanks import FillBlanks from .idea_match import IdeaMatch from .paragraph_match import ParagraphMatch @@ -138,13 +137,18 @@ class ReadingService(IReadingService): exercise_tasks = [] start_id = 1 + diff_helper = DifficultyHelper(dto.difficulty) + + none_count = sum(1 for ex in dto.exercises if ex.difficulty is None) + diff_helper.distribute_for_count(none_count) + for req_exercise in dto.exercises: exercise_tasks.append( self._generate_single_exercise( req_exercise, dto.text, start_id, - pick_difficulty(req_exercise.difficulty, dto.difficulty) + diff_helper.pick_difficulty(req_exercise.difficulty) ) ) start_id += req_exercise.quantity diff --git a/ielts_be/utils/__init__.py b/ielts_be/utils/__init__.py index e04a809..c396af2 100644 --- a/ielts_be/utils/__init__.py +++ b/ielts_be/utils/__init__.py @@ -1,9 +1,7 @@ from .handle_exception import handle_exception from .logger import suppress_loggers -from .pick_difficulty import pick_difficulty __all__ = [ "handle_exception", "suppress_loggers", - "pick_difficulty" ] diff --git a/ielts_be/utils/pick_difficulty.py b/ielts_be/utils/pick_difficulty.py deleted file mode 100644 index 0560dda..0000000 --- a/ielts_be/utils/pick_difficulty.py +++ /dev/null @@ -1,14 +0,0 @@ -import random -from typing import Optional, List - -from ielts_be.configs.constants import EducationalContent - - -def pick_difficulty(difficulty: Optional[str], difficulties: Optional[List[str]]) -> str: - if difficulty: - return difficulty - - if difficulties: - return random.choice(difficulties) - - return random.choice(EducationalContent.DIFFICULTIES)