Async release
This commit is contained in:
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
19
app/services/abc/__init__.py
Normal file
19
app/services/abc/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .level import ILevelService
|
||||
from .listening import IListeningService
|
||||
from .writing import IWritingService
|
||||
from .speaking import ISpeakingService
|
||||
from .reading import IReadingService
|
||||
from .grade import IGradeService
|
||||
from .training import ITrainingService
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"ILevelService",
|
||||
"IListeningService",
|
||||
"IWritingService",
|
||||
"ISpeakingService",
|
||||
"IReadingService",
|
||||
"IGradeService",
|
||||
"ITrainingService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
23
app/services/abc/grade.py
Normal file
23
app/services/abc/grade.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _parse_openai_response(response):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
pass
|
||||
24
app/services/abc/level.py
Normal file
24
app/services/abc/level.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_multiple_choice_level(self, quantity: int, start_id=1):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _replace_exercise_if_exists(self, all_exams, current_exercise, current_exam, seen_keys):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _generate_single_mc_level_question(self):
|
||||
pass
|
||||
68
app/services/abc/listening.py
Normal file
68
app/services/abc/listening.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class IListeningService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def generate_listening_question(self, section: int, topic: str) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_listening_exercises(
|
||||
self, section: int, dialog: str,
|
||||
req_exercises: list[str], exercises_queue: Queue,
|
||||
start_id: int, difficulty: str
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, parts, min_timer, difficulty):
|
||||
pass
|
||||
|
||||
# ==================================================================================================================
|
||||
# Helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
@abstractmethod
|
||||
async def _generate_listening_conversation(self, section: int, topic: str) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _generate_listening_monologue(self, section: int, topic: str) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_conversation_voices(self, response: Dict, unique_voices_across_segments: bool):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _get_random_voice(gender: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_multiple_choice_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_write_blanks_questions_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_write_blanks_notes_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_write_blanks_form_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
pass
|
||||
|
||||
49
app/services/abc/reading.py
Normal file
49
app/services/abc/reading.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
from app.configs.constants import QuestionType
|
||||
|
||||
|
||||
class IReadingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage(
|
||||
self,
|
||||
passage_id: int,
|
||||
topic: str,
|
||||
req_exercises: List[str],
|
||||
number_of_exercises_q: Queue,
|
||||
difficulty: str
|
||||
):
|
||||
pass
|
||||
|
||||
# ==================================================================================================================
|
||||
# Helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, q_type: QuestionType, topic: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _generate_reading_exercises(
|
||||
self, passage: str, req_exercises: list, number_of_exercises_q, start_id, difficulty
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_summary_fill_blanks_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_true_false_not_given_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_write_blanks_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _gen_paragraph_match_exercise(self, text: str, quantity: int, start_id):
|
||||
pass
|
||||
57
app/services/abc/speaking.py
Normal file
57
app/services/abc/speaking.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class ISpeakingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_task(self, task_id: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task_1_and_2(
|
||||
self, task: int, question: str, answer_firebase_path: str, sound_file_name: str
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task_3(self, answers: Dict, task: int = 3):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_speaking_video(self, original_question: str, topic: str, avatar: str, prompts: List[str]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_interactive_video(self, questions: List[str], avatar: str, topic: str):
|
||||
pass
|
||||
|
||||
# ==================================================================================================================
|
||||
# Helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _zero_rating(comment: str):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _calculate_overall(response: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _get_speaking_corrections(self, text):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _create_video_per_part(self, exercises: List[Dict], template: Dict, part: int):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _create_video(self, question: str, avatar: str, error_message: str):
|
||||
pass
|
||||
13
app/services/abc/third_parties/__init__.py
Normal file
13
app/services/abc/third_parties/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .stt import ISpeechToTextService
|
||||
from .tts import ITextToSpeechService
|
||||
from .llm import ILLMService
|
||||
from .vid_gen import IVideoGeneratorService
|
||||
from .ai_detector import IAIDetectorService
|
||||
|
||||
__all__ = [
|
||||
"ISpeechToTextService",
|
||||
"ITextToSpeechService",
|
||||
"ILLMService",
|
||||
"IVideoGeneratorService",
|
||||
"IAIDetectorService"
|
||||
]
|
||||
13
app/services/abc/third_parties/ai_detector.py
Normal file
13
app/services/abc/third_parties/ai_detector.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IAIDetectorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def run_detection(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
pass
|
||||
21
app/services/abc/third_parties/llm.py
Normal file
21
app/services/abc/third_parties/llm.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class ILLMService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def prediction_override(self, **kwargs):
|
||||
pass
|
||||
8
app/services/abc/third_parties/stt.py
Normal file
8
app/services/abc/third_parties/stt.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeechToTextService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def speech_to_text(self, file_path):
|
||||
pass
|
||||
22
app/services/abc/third_parties/tts.py
Normal file
22
app/services/abc/third_parties/tts.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ITextToSpeechService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str, output_format: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _text_to_speech(self, text: str):
|
||||
pass
|
||||
|
||||
10
app/services/abc/third_parties/vid_gen.py
Normal file
10
app/services/abc/third_parties/vid_gen.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.configs.constants import AvatarEnum
|
||||
|
||||
|
||||
class IVideoGeneratorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
pass
|
||||
13
app/services/abc/training.py
Normal file
13
app/services/abc/training.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ITrainingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _get_question_tips(question: str, answer: str, correct_answer: str, context: str = None):
|
||||
pass
|
||||
32
app/services/abc/writing.py
Normal file
32
app/services/abc/writing.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class IWritingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
pass
|
||||
|
||||
# ==================================================================================================================
|
||||
# Helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _get_writing_prompt(task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
async def _get_fixed_text(self, text):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _zero_rating(comment: str):
|
||||
pass
|
||||
19
app/services/impl/__init__.py
Normal file
19
app/services/impl/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .level import LevelService
|
||||
from .listening import ListeningService
|
||||
from .reading import ReadingService
|
||||
from .speaking import SpeakingService
|
||||
from .writing import WritingService
|
||||
from .grade import GradeService
|
||||
from .training import TrainingService
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"LevelService",
|
||||
"ListeningService",
|
||||
"ReadingService",
|
||||
"SpeakingService",
|
||||
"WritingService",
|
||||
"GradeService",
|
||||
"TrainingService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
156
app/services/impl/grade.py
Normal file
156
app/services/impl/grade.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import json
|
||||
from typing import List
|
||||
import copy
|
||||
|
||||
from app.services.abc import ILLMService, IGradeService
|
||||
|
||||
|
||||
class GradeService(IGradeService):
|
||||
|
||||
chat_config = {'max_tokens': 1000, 'temperature': 0.2}
|
||||
tools = [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_evaluation_and_suggestions",
|
||||
"description": "Saves the evaluation and suggestions requested by input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"evaluation": {
|
||||
"type": "string",
|
||||
"description": "A comment on the IELTS section grade obtained in the specific section and what it could mean without suggestions.",
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "string",
|
||||
"description": "A small paragraph text with suggestions on how to possibly get a better grade than the one obtained.",
|
||||
},
|
||||
"bullet_points": {
|
||||
"type": "string",
|
||||
"description": "Text with four bullet points to improve the english speaking ability. Only include text for the bullet points separated by a paragraph. ",
|
||||
},
|
||||
},
|
||||
"required": ["evaluation", "suggestions"],
|
||||
},
|
||||
}
|
||||
}]
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
ret = []
|
||||
|
||||
for section in extracted_sections:
|
||||
openai_response_dict = await self._calculate_section_grade_summary(section)
|
||||
ret.append(
|
||||
{
|
||||
'code': section['code'],
|
||||
'name': section['name'],
|
||||
'grade': section['grade'],
|
||||
'evaluation': openai_response_dict['evaluation'],
|
||||
'suggestions': openai_response_dict['suggestions'],
|
||||
'bullet_points': self._parse_bullet_points(openai_response_dict['bullet_points'], section['grade'])
|
||||
}
|
||||
)
|
||||
|
||||
return {'sections': ret}
|
||||
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
section_name = section['name']
|
||||
section_grade = section['grade']
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'You are a IELTS test section grade evaluator. You will receive a IELTS test section name and the '
|
||||
'grade obtained in the section. You should offer a evaluation comment on this grade and separately '
|
||||
'suggestions on how to possibly get a better grade.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Section: {str(section_name)} Grade: {str(section_grade)}',
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Speak in third person."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Don't offer suggestions in the evaluation comment. Only in the suggestions section."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your evaluation comment on the grade should enunciate the grade, be insightful, be speculative, "
|
||||
"be one paragraph long."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please save the evaluation comment and suggestions generated."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Offer bullet points to improve the english {str(section_name)} ability."
|
||||
},
|
||||
]
|
||||
|
||||
if section['code'] == "level":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is comprised of multiple choice questions that measure the user's overall english "
|
||||
"level. These multiple choice questions are about knowledge on vocabulary, syntax, grammar rules, "
|
||||
"and contextual usage. The grade obtained measures the ability in these areas and english language "
|
||||
"overall."
|
||||
)
|
||||
}]
|
||||
elif section['code'] == "speaking":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is s designed to assess the English language proficiency of individuals who want to "
|
||||
"study or work in English-speaking countries. The speaking section evaluates a candidate's ability "
|
||||
"to communicate effectively in spoken English."
|
||||
)
|
||||
}]
|
||||
|
||||
chat_config = copy.deepcopy(self.chat_config)
|
||||
tools = copy.deepcopy(self.tools)
|
||||
|
||||
res = await self._llm.prediction_override(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=chat_config['max_tokens'],
|
||||
temperature=chat_config['temperature'],
|
||||
tools=tools,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
return self._parse_openai_response(res)
|
||||
|
||||
@staticmethod
|
||||
def _parse_openai_response(response):
|
||||
if 'choices' in response and len(response['choices']) > 0 and 'message' in response['choices'][
|
||||
0] and 'tool_calls' in response['choices'][0]['message'] and isinstance(
|
||||
response['choices'][0]['message']['tool_calls'], list) and len(
|
||||
response['choices'][0]['message']['tool_calls']) > 0 and \
|
||||
response['choices'][0]['message']['tool_calls'][0]['function']['arguments']:
|
||||
return json.loads(response['choices'][0]['message']['tool_calls'][0]['function']['arguments'])
|
||||
else:
|
||||
return {'evaluation': "", 'suggestions': "", 'bullet_points': []}
|
||||
|
||||
@staticmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
max_grade_for_suggestions = 9
|
||||
if isinstance(bullet_points_str, str) and grade < max_grade_for_suggestions:
|
||||
# Split the string by '\n'
|
||||
lines = bullet_points_str.split('\n')
|
||||
|
||||
# Remove '-' and trim whitespace from each line
|
||||
cleaned_lines = [line.replace('-', '').strip() for line in lines]
|
||||
|
||||
# Add '.' to lines that don't end with it
|
||||
return [line + '.' if line and not line.endswith('.') else line for line in cleaned_lines]
|
||||
else:
|
||||
return []
|
||||
506
app/services/impl/level.py
Normal file
506
app/services/impl/level.py
Normal file
@@ -0,0 +1,506 @@
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings, EducationalContent, QuestionType
|
||||
from app.helpers import ExercisesHelper
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from app.services.abc import ILevelService, ILLMService, IReadingService
|
||||
|
||||
|
||||
class LevelService(ILevelService):
|
||||
|
||||
def __init__(
|
||||
self, llm: ILLMService, document_store: IDocumentStore, reading_service: IReadingService
|
||||
):
|
||||
self._llm = llm
|
||||
self._document_store = document_store
|
||||
self._reading_service = reading_service
|
||||
|
||||
async def get_level_exam(self):
|
||||
number_of_exercises = 25
|
||||
exercises = await self._gen_multiple_choice_level(number_of_exercises)
|
||||
return {
|
||||
"exercises": [exercises],
|
||||
"isDiagnostic": False,
|
||||
"minTimer": 25,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def _gen_multiple_choice_level(self, quantity: int, start_id=1):
|
||||
gen_multiple_choice_for_text = (
|
||||
f'Generate {str(quantity)} multiple choice questions of 4 options for an english level exam, some easy '
|
||||
'questions, some intermediate questions and some advanced questions. Ensure that the questions cover '
|
||||
'a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and '
|
||||
'punctuation. Make sure every question only has 1 correct answer.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"questions": [{"id": "9", "options": '
|
||||
'[{"id": "A", "text": "And"}, {"id": "B", "text": "Cat"}, '
|
||||
'{"id": "C", "text": "Happy"}, {"id": "D", "text": "Jump"}], '
|
||||
'"prompt": "Which of the following is a conjunction?", '
|
||||
'"solution": "A", "variant": "text"}]}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": gen_multiple_choice_for_text
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != quantity:
|
||||
return await self._gen_multiple_choice_level(quantity, start_id)
|
||||
else:
|
||||
all_exams = await self._document_store.get_all("level")
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
|
||||
all_exams, question["questions"][i], question, seen_keys
|
||||
)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": ExercisesHelper.fix_exercise_ids(question, start_id)["questions"],
|
||||
"type": "multipleChoice",
|
||||
}
|
||||
|
||||
async def _replace_exercise_if_exists(self, all_exams, current_exercise, current_exam, seen_keys):
|
||||
# Extracting relevant fields for comparison
|
||||
key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options'])))
|
||||
# Check if the key is in the set
|
||||
if key in seen_keys:
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_mc_level_question(), current_exam, seen_keys
|
||||
)
|
||||
else:
|
||||
seen_keys.add(key)
|
||||
|
||||
for exam in all_exams:
|
||||
exam_dict = exam.to_dict()
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exam_dict.get("exercises", [])[0]["questions"]
|
||||
):
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_mc_level_question(), current_exam, seen_keys
|
||||
)
|
||||
return current_exercise, seen_keys
|
||||
|
||||
async def _generate_single_mc_level_question(self):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"id": "9", "options": [{"id": "A", "text": "And"}, {"id": "B", "text": "Cat"}, '
|
||||
'{"id": "C", "text": "Happy"}, {"id": "D", "text": "Jump"}], '
|
||||
'"prompt": "Which of the following is a conjunction?", '
|
||||
'"solution": "A", "variant": "text"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Generate 1 multiple choice question of 4 options for an english level exam, it can be easy, '
|
||||
'intermediate or advanced.'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["options"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question
|
||||
|
||||
async def get_level_utas(self):
|
||||
# Formats
|
||||
mc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the correct word or group of words that completes the sentences.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 1
|
||||
}
|
||||
|
||||
umc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the underlined word or group of words that is not correct.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 2
|
||||
}
|
||||
|
||||
bs_1 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 3
|
||||
}
|
||||
|
||||
bs_2 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 4
|
||||
}
|
||||
|
||||
reading = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and answer the questions below.",
|
||||
"questions": None,
|
||||
"type": "readingExercises",
|
||||
"part": 5
|
||||
}
|
||||
|
||||
all_mc_questions = []
|
||||
|
||||
# PART 1
|
||||
mc_exercises1 = await self._gen_multiple_choice_blank_space_utas(15, 1, all_mc_questions)
|
||||
print(json.dumps(mc_exercises1, indent=4))
|
||||
all_mc_questions.append(mc_exercises1)
|
||||
|
||||
# PART 2
|
||||
mc_exercises2 = await self._gen_multiple_choice_blank_space_utas(15, 16, all_mc_questions)
|
||||
print(json.dumps(mc_exercises2, indent=4))
|
||||
all_mc_questions.append(mc_exercises2)
|
||||
|
||||
# PART 3
|
||||
mc_exercises3 = await self._gen_multiple_choice_blank_space_utas(15, 31, all_mc_questions)
|
||||
print(json.dumps(mc_exercises3, indent=4))
|
||||
all_mc_questions.append(mc_exercises3)
|
||||
|
||||
mc_exercises = mc_exercises1['questions'] + mc_exercises2['questions'] + mc_exercises3['questions']
|
||||
print(json.dumps(mc_exercises, indent=4))
|
||||
mc["questions"] = mc_exercises
|
||||
|
||||
# Underlined mc
|
||||
underlined_mc = await self._gen_multiple_choice_underlined_utas(15, 46)
|
||||
print(json.dumps(underlined_mc, indent=4))
|
||||
umc["questions"] = underlined_mc
|
||||
|
||||
# Blank Space text 1
|
||||
blank_space_text_1 = await self._gen_blank_space_text_utas(12, 61, 250)
|
||||
print(json.dumps(blank_space_text_1, indent=4))
|
||||
bs_1["questions"] = blank_space_text_1
|
||||
|
||||
# Blank Space text 2
|
||||
blank_space_text_2 = await self._gen_blank_space_text_utas(14, 73, 350)
|
||||
print(json.dumps(blank_space_text_2, indent=4))
|
||||
bs_2["questions"] = blank_space_text_2
|
||||
|
||||
# Reading text
|
||||
reading_text = await self._gen_reading_passage_utas(87, 10, 4)
|
||||
print(json.dumps(reading_text, indent=4))
|
||||
reading["questions"] = reading_text
|
||||
|
||||
return {
|
||||
"exercises": {
|
||||
"blankSpaceMultipleChoice": mc,
|
||||
"underlinedMultipleChoice": umc,
|
||||
"blankSpaceText1": bs_1,
|
||||
"blankSpaceText2": bs_2,
|
||||
"readingExercises": reading,
|
||||
},
|
||||
"isDiagnostic": False,
|
||||
"minTimer": 25,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def _gen_multiple_choice_blank_space_utas(self, quantity: int, start_id: int, all_exams):
|
||||
gen_multiple_choice_for_text = (
|
||||
f'Generate {str(quantity)} multiple choice blank space questions of 4 options for an english '
|
||||
'level exam, some easy questions, some intermediate questions and some advanced questions. Ensure '
|
||||
'that the questions cover a range of topics such as verb tense, subject-verb agreement, pronoun usage, '
|
||||
'sentence structure, and punctuation. Make sure every question only has 1 correct answer.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"questions": [{"id": "9", "options": [{"id": "A", "text": '
|
||||
'"And"}, {"id": "B", "text": "Cat"}, {"id": "C", "text": '
|
||||
'"Happy"}, {"id": "D", "text": "Jump"}], '
|
||||
'"prompt": "Which of the following is a conjunction?", '
|
||||
'"solution": "A", "variant": "text"}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": gen_multiple_choice_for_text
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != quantity:
|
||||
return await self._gen_multiple_choice_level(quantity, start_id)
|
||||
else:
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists_utas(
|
||||
all_exams,
|
||||
question["questions"][i],
|
||||
question,
|
||||
seen_keys
|
||||
)
|
||||
return ExercisesHelper.fix_exercise_ids(question, start_id)
|
||||
|
||||
async def _replace_exercise_if_exists_utas(self, all_exams, current_exercise, current_exam, seen_keys):
|
||||
# Extracting relevant fields for comparison
|
||||
key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options'])))
|
||||
# Check if the key is in the set
|
||||
if key in seen_keys:
|
||||
return self._replace_exercise_if_exists_utas(
|
||||
all_exams, await self._generate_single_mc_level_question(), current_exam, seen_keys
|
||||
)
|
||||
else:
|
||||
seen_keys.add(key)
|
||||
|
||||
for exam in all_exams:
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exam.get("questions", [])
|
||||
):
|
||||
return self._replace_exercise_if_exists_utas(
|
||||
all_exams, await self._generate_single_mc_level_question(), current_exam, seen_keys
|
||||
)
|
||||
return current_exercise, seen_keys
|
||||
|
||||
|
||||
async def _gen_multiple_choice_underlined_utas(self, quantity: int, start_id: int):
|
||||
json_format = {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
gen_multiple_choice_for_text = (
|
||||
f'Generate {str(quantity)} multiple choice questions of 4 options for an english '
|
||||
'level exam, some easy questions, some intermediate questions and some advanced questions. Ensure that '
|
||||
'the questions cover a range of topics such as verb tense, subject-verb agreement, pronoun usage, '
|
||||
'sentence structure, and punctuation. Make sure every question only has 1 correct answer.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": gen_multiple_choice_for_text
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'The type of multiple choice is the prompt has wrong words or group of words and the options '
|
||||
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
|
||||
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
|
||||
'the boss <u>is</u> nice."\nOptions:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != quantity:
|
||||
return await self._gen_multiple_choice_level(quantity, start_id)
|
||||
else:
|
||||
return ExercisesHelper.fix_exercise_ids(question, start_id)["questions"]
|
||||
|
||||
async def _gen_blank_space_text_utas(
|
||||
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
json_format = {
|
||||
"question": {
|
||||
"words": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"text": "text"
|
||||
}
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Generate a text of at least {str(size)} words about the topic {topic}.'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'From the generated text choose {str(quantity)} words (cannot be sequential words) to replace '
|
||||
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
|
||||
'The ids must be ordered throughout the text and the words must be replaced only once. Put '
|
||||
'the removed words and respective ids on the words array of the json in the correct order.'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question["question"]
|
||||
|
||||
async def _gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
|
||||
passage = await self._reading_service.generate_reading_passage(QuestionType.READING_PASSAGE_1, topic)
|
||||
short_answer = await self._gen_short_answer_utas(passage["text"], start_id, sa_quantity)
|
||||
mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id + sa_quantity, mc_quantity)
|
||||
return {
|
||||
"exercises": {
|
||||
"shortAnswer": short_answer,
|
||||
"multipleChoice": mc_exercises,
|
||||
},
|
||||
"text": {
|
||||
"content": passage["text"],
|
||||
"title": passage["title"]
|
||||
}
|
||||
}
|
||||
|
||||
async def _gen_short_answer_utas(self, text: str, start_id: int, sa_quantity: int):
|
||||
json_format = {"questions": [{"id": 1, "question": "question", "possible_answers": ["answer_1", "answer_2"]}]}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Generate ' + str(sa_quantity) + ' short answer questions, and the possible answers, must have '
|
||||
'maximum 3 words per answer, about this text:\n"' + text + '"')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": 'The id starts at ' + str(start_id) + '.'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
)["questions"]
|
||||
|
||||
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
|
||||
json_format = {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": 'Generate ' + str(
|
||||
mc_quantity) + ' multiple choice questions of 4 options for this text:\n' + text
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": 'Make sure every question only has 1 correct answer.'
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != mc_quantity:
|
||||
return await self._gen_multiple_choice_level(mc_quantity, start_id)
|
||||
else:
|
||||
return ExercisesHelper.fix_exercise_ids(question, start_id)["questions"]
|
||||
393
app/services/impl/listening.py
Normal file
393
app/services/impl/listening.py
Normal file
@@ -0,0 +1,393 @@
|
||||
import uuid
|
||||
from queue import Queue
|
||||
import random
|
||||
from typing import Dict
|
||||
|
||||
from app.repositories.abc import IFileStorage, IDocumentStore
|
||||
from app.services.abc import IListeningService, ILLMService, ITextToSpeechService
|
||||
from app.configs.question_templates import getListeningTemplate, getListeningPartTemplate
|
||||
from app.configs.constants import (
|
||||
NeuralVoices, GPTModels, TemperatureSettings, FilePaths, MinTimers, ExamVariant
|
||||
)
|
||||
from app.helpers import ExercisesHelper
|
||||
|
||||
|
||||
class ListeningService(IListeningService):
|
||||
|
||||
CONVERSATION_TAIL = (
|
||||
"Please include random names and genders for the characters in your dialogue. "
|
||||
"Make sure that the generated conversation does not contain forbidden subjects in muslim countries."
|
||||
)
|
||||
|
||||
MONOLOGUE_TAIL = (
|
||||
"Make sure that the generated monologue does not contain forbidden subjects in muslim countries."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, llm: ILLMService,
|
||||
tts: ITextToSpeechService,
|
||||
file_storage: IFileStorage,
|
||||
document_store: IDocumentStore
|
||||
):
|
||||
self._llm = llm
|
||||
self._tts = tts
|
||||
self._file_storage = file_storage
|
||||
self._document_store = document_store
|
||||
self._sections = {
|
||||
"section_1": {
|
||||
"generate_dialogue": self._generate_listening_conversation,
|
||||
"type": "conversation"
|
||||
},
|
||||
"section_2": {
|
||||
"generate_dialogue": self._generate_listening_monologue,
|
||||
"type": "monologue"
|
||||
},
|
||||
"section_3": {
|
||||
"generate_dialogue": self._generate_listening_conversation,
|
||||
"type": "conversation"
|
||||
},
|
||||
"section_4": {
|
||||
"generate_dialogue": self._generate_listening_monologue,
|
||||
"type": "monologue"
|
||||
}
|
||||
}
|
||||
|
||||
async def generate_listening_question(self, section: int, topic: str):
|
||||
return await self._sections[f'section_{section}']["generate_dialogue"](section, topic)
|
||||
|
||||
async def generate_listening_exercises(
|
||||
self, section: int, dialog: str,
|
||||
req_exercises: list[str], number_of_exercises_q: Queue,
|
||||
start_id: int, difficulty: str
|
||||
):
|
||||
dialog_type = self._sections[f'section_{section}']["type"]
|
||||
|
||||
exercises = []
|
||||
|
||||
for req_exercise in req_exercises:
|
||||
number_of_exercises = number_of_exercises_q.get()
|
||||
|
||||
if req_exercise == "multipleChoice":
|
||||
question = await self._gen_multiple_choice_exercise_listening(
|
||||
dialog_type, dialog, number_of_exercises, start_id, difficulty
|
||||
)
|
||||
|
||||
exercises.append(question)
|
||||
print("Added multiple choice: " + str(question))
|
||||
elif req_exercise == "writeBlanksQuestions":
|
||||
question = await self._gen_write_blanks_questions_exercise_listening(
|
||||
dialog_type, dialog, number_of_exercises, start_id, difficulty
|
||||
)
|
||||
|
||||
exercises.append(question)
|
||||
print("Added write blanks questions: " + str(question))
|
||||
elif req_exercise == "writeBlanksFill":
|
||||
question = await self._gen_write_blanks_notes_exercise_listening(
|
||||
dialog_type, dialog, number_of_exercises, start_id, difficulty
|
||||
)
|
||||
|
||||
exercises.append(question)
|
||||
print("Added write blanks notes: " + str(question))
|
||||
elif req_exercise == "writeBlanksForm":
|
||||
question = await self._gen_write_blanks_form_exercise_listening(
|
||||
dialog_type, dialog, number_of_exercises, start_id, difficulty
|
||||
)
|
||||
|
||||
exercises.append(question)
|
||||
print("Added write blanks form: " + str(question))
|
||||
|
||||
start_id = start_id + number_of_exercises
|
||||
|
||||
return exercises
|
||||
|
||||
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str):
|
||||
template = getListeningTemplate()
|
||||
template['difficulty'] = difficulty
|
||||
listening_id = str(uuid.uuid4())
|
||||
for i, part in enumerate(parts, start=0):
|
||||
part_template = getListeningPartTemplate()
|
||||
|
||||
file_name = str(uuid.uuid4()) + ".mp3"
|
||||
sound_file_path = FilePaths.AUDIO_FILES_PATH + file_name
|
||||
firebase_file_path = FilePaths.FIREBASE_LISTENING_AUDIO_FILES_PATH + file_name
|
||||
if "conversation" in part["text"]:
|
||||
await self._tts.text_to_speech(part["text"]["conversation"], sound_file_path)
|
||||
else:
|
||||
await self._tts.text_to_speech(part["text"], sound_file_path)
|
||||
file_url = await self._file_storage.upload_file_firebase_get_url(firebase_file_path, sound_file_path)
|
||||
|
||||
part_template["audio"]["source"] = file_url
|
||||
part_template["exercises"] = part["exercises"]
|
||||
|
||||
template['parts'].append(part_template)
|
||||
|
||||
if min_timer != MinTimers.LISTENING_MIN_TIMER_DEFAULT:
|
||||
template["minTimer"] = min_timer
|
||||
template["variant"] = ExamVariant.PARTIAL.value
|
||||
else:
|
||||
template["variant"] = ExamVariant.FULL.value
|
||||
|
||||
(result, listening_id) = await self._document_store.save_to_db_with_id("listening", template, listening_id)
|
||||
if result:
|
||||
return {**template, "id": listening_id}
|
||||
else:
|
||||
raise Exception("Failed to save question: " + str(parts))
|
||||
|
||||
# ==================================================================================================================
|
||||
# generate_listening_question helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
async def _generate_listening_conversation(self, section: int, topic: str) -> Dict:
|
||||
head = (
|
||||
'Compose an authentic conversation between two individuals in the everyday social context of "'
|
||||
if section == 1 else
|
||||
'Compose an authentic and elaborate conversation between up to four individuals in the everyday '
|
||||
'social context of "'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'{head}{topic}". {self.CONVERSATION_TAIL}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["conversation"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return self._get_conversation_voices(response, True)
|
||||
|
||||
async def _generate_listening_monologue(self, section: int, topic: str) -> Dict:
|
||||
context = 'social context' if section == 2 else 'academic subject'
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"monologue": "monologue"}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate a comprehensive monologue set in the {context} of "{topic}". {self.MONOLOGUE_TAIL}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["monologue"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
return response["monologue"]
|
||||
|
||||
def _get_conversation_voices(self, response: Dict, unique_voices_across_segments: bool):
|
||||
chosen_voices = []
|
||||
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:
|
||||
voice = None
|
||||
# section 1
|
||||
if unique_voices_across_segments:
|
||||
while voice is None:
|
||||
chosen_voice = self._get_random_voice(segment['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'])
|
||||
name_to_voice[name] = voice
|
||||
segment['voice'] = voice
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _get_random_voice(gender: str):
|
||||
if gender.lower() == 'male':
|
||||
available_voices = NeuralVoices.MALE_NEURAL_VOICES
|
||||
else:
|
||||
available_voices = NeuralVoices.FEMALE_NEURAL_VOICES
|
||||
|
||||
return random.choice(available_voices)['Id']
|
||||
|
||||
# ==================================================================================================================
|
||||
# generate_listening_exercises helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
async def _gen_multiple_choice_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"questions": [{"id": "9", "options": [{"id": "A", "text": "Economic benefits"}, {"id": "B", "text": '
|
||||
'"Government regulations"}, {"id": "C", "text": "Concerns about climate change"}, {"id": "D", "text": '
|
||||
'"Technological advancement"}], "prompt": "What is the main reason for the shift towards renewable '
|
||||
'energy sources?", "solution": "C", "variant": "text"}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {str(quantity)} {difficulty} difficulty multiple choice questions of 4 options '
|
||||
f'for this {dialog_type}:\n"' + text + '"')
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
questions = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["questions"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": ExercisesHelper.fix_exercise_ids(questions, start_id)["questions"],
|
||||
"type": "multipleChoice",
|
||||
}
|
||||
|
||||
async def _gen_write_blanks_questions_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {str(quantity)} {difficulty} difficulty short answer questions, and the '
|
||||
f'possible answers (max 3 words per answer), about this {dialog_type}:\n"{text}"')
|
||||
}
|
||||
]
|
||||
|
||||
questions = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
questions = questions["questions"][:quantity]
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"maxWords": 3,
|
||||
"prompt": f"You will hear a {dialog_type}. Answer the questions below using no more than three words or a number accordingly.",
|
||||
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
|
||||
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
|
||||
"type": "writeBlanks"
|
||||
}
|
||||
|
||||
async def _gen_write_blanks_notes_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"notes": ["note_1", "note_2"]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {str(quantity)} {difficulty} difficulty notes taken from this '
|
||||
f'{dialog_type}:\n"{text}"'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
questions = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["notes"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
questions = questions["notes"][:quantity]
|
||||
|
||||
formatted_phrases = "\n".join([f"{i + 1}. {phrase}" for i, phrase in enumerate(questions)])
|
||||
|
||||
word_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this '
|
||||
'format: {"words": ["word_1", "word_2"] }'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": ('Select 1 word from each phrase in this list:\n"' + formatted_phrases + '"')
|
||||
|
||||
}
|
||||
]
|
||||
words = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, word_messages, ["words"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
words = words["words"][:quantity]
|
||||
|
||||
replaced_notes = ExercisesHelper.replace_first_occurrences_with_placeholders_notes(questions, words, start_id)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"maxWords": 3,
|
||||
"prompt": "Fill the blank space with the word missing from the audio.",
|
||||
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
|
||||
"text": "\\n".join(replaced_notes),
|
||||
"type": "writeBlanks"
|
||||
}
|
||||
|
||||
async def _gen_write_blanks_form_exercise_listening(
|
||||
self, dialog_type: str, text: str, quantity: int, start_id, difficulty
|
||||
):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"form": ["key: value", "key2: value"]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate a form with {str(quantity)} {difficulty} difficulty key-value pairs '
|
||||
f'about this {dialog_type}:\n"{text}"'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
parsed_form = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["form"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
parsed_form = parsed_form["form"][:quantity]
|
||||
|
||||
replaced_form, words = ExercisesHelper.build_write_blanks_text_form(parsed_form, start_id)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"maxWords": 3,
|
||||
"prompt": f"You will hear a {dialog_type}. Fill the form with words/numbers missing.",
|
||||
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
|
||||
"text": replaced_form,
|
||||
"type": "writeBlanks"
|
||||
}
|
||||
|
||||
287
app/services/impl/reading.py
Normal file
287
app/services/impl/reading.py
Normal file
@@ -0,0 +1,287 @@
|
||||
import random
|
||||
import uuid
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
from app.services.abc import IReadingService, ILLMService
|
||||
from app.configs.constants import QuestionType, TemperatureSettings, FieldsAndExercises, GPTModels
|
||||
from app.helpers import ExercisesHelper
|
||||
|
||||
|
||||
class ReadingService(IReadingService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
self._passages = {
|
||||
"passage_1": {
|
||||
"question_type": QuestionType.READING_PASSAGE_1,
|
||||
"start_id": 1
|
||||
},
|
||||
"passage_2": {
|
||||
"question_type": QuestionType.READING_PASSAGE_2,
|
||||
"start_id": 14
|
||||
},
|
||||
"passage_3": {
|
||||
"question_type": QuestionType.READING_PASSAGE_3,
|
||||
"start_id": 27
|
||||
}
|
||||
}
|
||||
|
||||
async def gen_reading_passage(
|
||||
self,
|
||||
passage_id: int,
|
||||
topic: str,
|
||||
req_exercises: List[str],
|
||||
number_of_exercises_q: Queue,
|
||||
difficulty: str
|
||||
):
|
||||
_passage = self._passages[f'passage_{str(passage_id)}']
|
||||
|
||||
passage = await self.generate_reading_passage(_passage["question_type"], topic)
|
||||
|
||||
if passage == "":
|
||||
return await self.gen_reading_passage(passage_id, topic, req_exercises, number_of_exercises_q, difficulty)
|
||||
|
||||
start_id = _passage["start_id"]
|
||||
exercises = await self._generate_reading_exercises(
|
||||
passage["text"], req_exercises, number_of_exercises_q, start_id, difficulty
|
||||
)
|
||||
if ExercisesHelper.contains_empty_dict(exercises):
|
||||
return await self.gen_reading_passage(passage_id, topic, req_exercises, number_of_exercises_q, difficulty)
|
||||
|
||||
return {
|
||||
"exercises": exercises,
|
||||
"text": {
|
||||
"content": passage["text"],
|
||||
"title": passage["title"]
|
||||
},
|
||||
"difficulty": difficulty
|
||||
}
|
||||
|
||||
async def generate_reading_passage(self, q_type: QuestionType, topic: str):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"title": "title of the text", "text": "generated text"}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate an extensive text for IELTS {q_type.value}, of at least 1500 words, '
|
||||
f'on the topic of "{topic}". The passage should offer a substantial amount of '
|
||||
'information, analysis, or narrative relevant to the chosen subject matter. This text '
|
||||
'passage aims to serve as the primary reading section of an IELTS test, providing an '
|
||||
'in-depth and comprehensive exploration of the topic. Make sure that the generated text '
|
||||
'does not contain forbidden subjects in muslim countries.'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
FieldsAndExercises.GEN_TEXT_FIELDS,
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
async def _generate_reading_exercises(
|
||||
self, passage: str, req_exercises: list, number_of_exercises_q, start_id, difficulty
|
||||
):
|
||||
exercises = []
|
||||
for req_exercise in req_exercises:
|
||||
number_of_exercises = number_of_exercises_q.get()
|
||||
|
||||
if req_exercise == "fillBlanks":
|
||||
question = await self._gen_summary_fill_blanks_exercise(passage, number_of_exercises, start_id, difficulty)
|
||||
exercises.append(question)
|
||||
print("Added fill blanks: " + str(question))
|
||||
elif req_exercise == "trueFalse":
|
||||
question = await self._gen_true_false_not_given_exercise(passage, number_of_exercises, start_id, difficulty)
|
||||
exercises.append(question)
|
||||
print("Added trueFalse: " + str(question))
|
||||
elif req_exercise == "writeBlanks":
|
||||
question = await self._gen_write_blanks_exercise(passage, number_of_exercises, start_id, difficulty)
|
||||
if ExercisesHelper.answer_word_limit_ok(question):
|
||||
exercises.append(question)
|
||||
print("Added write blanks: " + str(question))
|
||||
else:
|
||||
exercises.append({})
|
||||
print("Did not add write blanks because it did not respect word limit")
|
||||
elif req_exercise == "paragraphMatch":
|
||||
question = await self._gen_paragraph_match_exercise(passage, number_of_exercises, start_id)
|
||||
exercises.append(question)
|
||||
print("Added paragraph match: " + str(question))
|
||||
|
||||
start_id = start_id + number_of_exercises
|
||||
|
||||
return exercises
|
||||
|
||||
async def _gen_summary_fill_blanks_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{ "summary": "summary", "words": ["word_1", "word_2"] }')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Summarize this text: "{text}"'
|
||||
)
|
||||
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Select {str(quantity)} {difficulty} difficulty words, it must be words and not '
|
||||
'expressions, from the summary.'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["summary"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
replaced_summary = ExercisesHelper.replace_first_occurrences_with_placeholders(response["summary"], response["words"], start_id)
|
||||
options_words = ExercisesHelper.add_random_words_and_shuffle(response["words"], 5)
|
||||
solutions = ExercisesHelper.fillblanks_build_solutions_array(response["words"], start_id)
|
||||
|
||||
return {
|
||||
"allowRepetition": True,
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": (
|
||||
"Complete the summary below. Click a blank to select the corresponding word(s) for it.\\nThere are "
|
||||
"more words than spaces so you will not use them all. You may use any of the words more than once."
|
||||
),
|
||||
"solutions": solutions,
|
||||
"text": replaced_summary,
|
||||
"type": "fillBlanks",
|
||||
"words": options_words
|
||||
|
||||
}
|
||||
|
||||
async def _gen_true_false_not_given_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"prompts":[{"prompt": "statement_1", "solution": "true/false/not_given"}, '
|
||||
'{"prompt": "statement_2", "solution": "true/false/not_given"}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {str(quantity)} {difficulty} difficulty statements based on the provided text. '
|
||||
'Ensure that your statements accurately represent information or inferences from the text, and '
|
||||
'provide a variety of responses, including, at least one of each True, False, and Not Given, '
|
||||
f'as appropriate.\n\nReference text:\n\n {text}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["prompts"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
questions = response["prompts"]
|
||||
|
||||
if len(questions) > quantity:
|
||||
questions = ExercisesHelper.remove_excess_questions(questions, len(questions) - quantity)
|
||||
|
||||
for i, question in enumerate(questions, start=start_id):
|
||||
question["id"] = str(i)
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Do the following statements agree with the information given in the Reading Passage?",
|
||||
"questions": questions,
|
||||
"type": "trueFalse"
|
||||
}
|
||||
|
||||
async def _gen_write_blanks_exercise(self, text: str, quantity: int, start_id, difficulty):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {str(quantity)} {difficulty} difficulty short answer questions, and the '
|
||||
f'possible answers, must have maximum 3 words per answer, about this text:\n"{text}"'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
questions = response["questions"][:quantity]
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"maxWords": 3,
|
||||
"prompt": "Choose no more than three words and/or a number from the passage for each answer.",
|
||||
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
|
||||
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
|
||||
"type": "writeBlanks"
|
||||
}
|
||||
|
||||
async def _gen_paragraph_match_exercise(self, text: str, quantity: int, start_id):
|
||||
paragraphs = ExercisesHelper.assign_letters_to_paragraphs(text)
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"headings": [ {"heading": "first paragraph heading"}, {"heading": "second paragraph heading"}]}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'For every paragraph of the list generate a minimum 5 word heading for it. '
|
||||
f'The paragraphs are these: {str(paragraphs)}'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["headings"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
headings = response["headings"]
|
||||
|
||||
options = []
|
||||
for i, paragraph in enumerate(paragraphs, start=0):
|
||||
paragraph["heading"] = headings[i]
|
||||
options.append({
|
||||
"id": paragraph["letter"],
|
||||
"sentence": paragraph["paragraph"]
|
||||
})
|
||||
|
||||
random.shuffle(paragraphs)
|
||||
sentences = []
|
||||
for i, paragraph in enumerate(paragraphs, start=start_id):
|
||||
sentences.append({
|
||||
"id": i,
|
||||
"sentence": paragraph["heading"],
|
||||
"solution": paragraph["letter"]
|
||||
})
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"allowRepetition": False,
|
||||
"options": options,
|
||||
"prompt": "Choose the correct heading for paragraphs from the list of headings below.",
|
||||
"sentences": sentences[:quantity],
|
||||
"type": "matchSentences"
|
||||
}
|
||||
521
app/services/impl/speaking.py
Normal file
521
app/services/impl/speaking.py
Normal file
@@ -0,0 +1,521 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import random
|
||||
from typing import Dict, List
|
||||
|
||||
from app.repositories.abc import IFileStorage, IDocumentStore
|
||||
from app.services.abc import ISpeakingService, ILLMService, IVideoGeneratorService, ISpeechToTextService
|
||||
from app.configs.constants import (
|
||||
FieldsAndExercises, GPTModels, TemperatureSettings,
|
||||
AvatarEnum, FilePaths
|
||||
)
|
||||
from app.helpers import TextHelper
|
||||
|
||||
|
||||
class SpeakingService(ISpeakingService):
|
||||
|
||||
def __init__(
|
||||
self, llm: ILLMService, vid_gen: IVideoGeneratorService,
|
||||
file_storage: IFileStorage, document_store: IDocumentStore,
|
||||
stt: ISpeechToTextService
|
||||
):
|
||||
self._llm = llm
|
||||
self._vid_gen = vid_gen
|
||||
self._file_storage = file_storage
|
||||
self._document_store = document_store
|
||||
self._stt = stt
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._tasks = {
|
||||
"task_1": {
|
||||
"get": {
|
||||
"json_template": (
|
||||
'{"topic": "topic", "question": "question"}'
|
||||
),
|
||||
"prompt": (
|
||||
'Craft a thought-provoking question of {difficulty} difficulty for IELTS Speaking Part 1 '
|
||||
'that encourages candidates to delve deeply into personal experiences, preferences, or '
|
||||
'insights on the topic of "{topic}". Instruct the candidate to offer not only detailed '
|
||||
'descriptions but also provide nuanced explanations, examples, or anecdotes to enrich '
|
||||
'their response. Make sure that the generated question does not contain forbidden subjects in '
|
||||
'muslim countries.'
|
||||
)
|
||||
}
|
||||
},
|
||||
"task_2": {
|
||||
"get": {
|
||||
"json_template": (
|
||||
'{"topic": "topic", "question": "question", "prompts": ["prompt_1", "prompt_2", "prompt_3"]}'
|
||||
),
|
||||
"prompt": (
|
||||
'Create a question of {difficulty} difficulty for IELTS Speaking Part 2 '
|
||||
'that encourages candidates to narrate a personal experience or story related to the topic '
|
||||
'of "{topic}". Include 3 prompts that guide the candidate to describe '
|
||||
'specific aspects of the experience, such as details about the situation, '
|
||||
'their actions, and the reasons it left a lasting impression. Make sure that the '
|
||||
'generated question does not contain forbidden subjects in muslim countries.'
|
||||
)
|
||||
}
|
||||
},
|
||||
"task_3": {
|
||||
"get": {
|
||||
"json_template": (
|
||||
'{"topic": "topic", "questions": ["question", "question", "question"]}'
|
||||
),
|
||||
"prompt": (
|
||||
'Formulate a set of 3 questions of {difficulty} difficulty for IELTS Speaking Part 3 '
|
||||
'that encourage candidates to engage in a meaningful discussion on the topic of "{topic}". '
|
||||
'Provide inquiries, ensuring they explore various aspects, perspectives, and implications '
|
||||
'related to the topic. Make sure that the generated question does not contain forbidden '
|
||||
'subjects in muslim countries.'
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async def get_speaking_task(self, task_id: int, topic: str, difficulty: str):
|
||||
task_values = self._tasks[f'task_{task_id}']['get']
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: ' +
|
||||
task_values["json_template"]
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": str(task_values["prompt"]).format(topic=topic, difficulty=difficulty)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, FieldsAndExercises.GEN_FIELDS, TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
# TODO: this was on GET /speaking_task_3 don't know if it is intentional only for 3
|
||||
if task_id == 3:
|
||||
# Remove the numbers from the questions only if the string starts with a number
|
||||
response["questions"] = [
|
||||
re.sub(r"^\d+\.\s*", "", question)
|
||||
if re.match(r"^\d+\.", question) else question
|
||||
for question in response["questions"]
|
||||
]
|
||||
|
||||
response["type"] = task_id
|
||||
response["difficulty"] = difficulty
|
||||
response["topic"] = topic
|
||||
return response
|
||||
|
||||
async def grade_speaking_task_1_and_2(
|
||||
self, task: int, question: str, answer_firebase_path: str, sound_file_name: str
|
||||
):
|
||||
request_id = uuid.uuid4()
|
||||
req_data = {
|
||||
"question": question,
|
||||
"answer": answer_firebase_path
|
||||
}
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - Received request to grade speaking task {task}. '
|
||||
f'Use this id to track the logs: {str(request_id)} - Request data: {str(req_data)}'
|
||||
)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Downloading file {answer_firebase_path}')
|
||||
|
||||
await self._file_storage.download_firebase_file(answer_firebase_path, sound_file_name)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Downloaded file {answer_firebase_path} to {sound_file_name}')
|
||||
|
||||
answer = await self._stt.speech_to_text(sound_file_name)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Transcripted answer: {answer}')
|
||||
|
||||
if TextHelper.has_x_words(answer, 20):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"comment": "comment about answer quality", "overall": 0.0, '
|
||||
'"task_response": {"Fluency and Coherence": 0.0, "Lexical Resource": 0.0, '
|
||||
'"Grammatical Range and Accuracy": 0.0, "Pronunciation": 0.0}}')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Evaluate the given Speaking Part {task} response based on the IELTS grading system, ensuring a '
|
||||
'strict assessment that penalizes errors. Deduct points for deviations from the task, and '
|
||||
'assign a score of 0 if the response fails to address the question. Additionally, provide '
|
||||
'detailed commentary highlighting both strengths and weaknesses in the response.'
|
||||
f'\n Question: "{question}" \n Answer: "{answer}"')
|
||||
}
|
||||
]
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Requesting grading of the answer.')
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
["comment"],
|
||||
TemperatureSettings.GRADING_TEMPERATURE
|
||||
)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Answer graded: {str(response)}')
|
||||
|
||||
perfect_answer_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"answer": "perfect answer"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Provide a perfect answer according to ielts grading system to the following '
|
||||
f'Speaking Part {task} question: "{question}"')
|
||||
}
|
||||
]
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Requesting perfect answer.')
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
perfect_answer_messages,
|
||||
["answer"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
response['perfect_answer'] = response["answer"]
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Perfect answer: ' + response['perfect_answer'])
|
||||
|
||||
response['transcript'] = answer
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Requesting fixed text.')
|
||||
|
||||
response['fixed_text'] = await self._get_speaking_corrections(answer)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Fixed text: ' + response['fixed_text'])
|
||||
|
||||
if response["overall"] == "0.0" or response["overall"] == 0.0:
|
||||
response["overall"] = self._calculate_overall(response)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Final response: {str(response)}')
|
||||
return response
|
||||
else:
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - '
|
||||
f'The answer had less words than threshold 20 to be graded. Answer: {answer}'
|
||||
)
|
||||
|
||||
return self._zero_rating("The audio recorded does not contain enough english words to be graded.")
|
||||
|
||||
# TODO: When there's more time grade_speaking_task_1_2 can be merged with this, when there's more time
|
||||
async def grade_speaking_task_3(self, answers: Dict, task: int = 3):
|
||||
request_id = uuid.uuid4()
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - Received request to grade speaking task {task}. '
|
||||
f'Use this id to track the logs: {str(request_id)} - Request data: {str(answers)}'
|
||||
)
|
||||
|
||||
text_answers = []
|
||||
perfect_answers = []
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - Received {str(len(answers))} total answers.'
|
||||
)
|
||||
for item in answers:
|
||||
sound_file_name = FilePaths.AUDIO_FILES_PATH + str(uuid.uuid4())
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Downloading file {item["answer"]}')
|
||||
|
||||
await self._file_storage.download_firebase_file(item["answer"], sound_file_name)
|
||||
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - '
|
||||
'Downloaded file ' + item["answer"] + f' to {sound_file_name}'
|
||||
)
|
||||
|
||||
answer_text = await self._stt.speech_to_text(sound_file_name)
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Transcripted answer: {answer_text}')
|
||||
|
||||
text_answers.append(answer_text)
|
||||
item["answer"] = answer_text
|
||||
os.remove(sound_file_name)
|
||||
|
||||
if not TextHelper.has_x_words(answer_text, 20):
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - '
|
||||
f'The answer had less words than threshold 20 to be graded. Answer: {answer_text}')
|
||||
return self._zero_rating("The audio recorded does not contain enough english words to be graded.")
|
||||
|
||||
perfect_answer_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"answer": "perfect answer"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Provide a perfect answer according to ielts grading system to the following '
|
||||
f'Speaking Part {task} question: "{item["question"]}"'
|
||||
)
|
||||
}
|
||||
]
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - '
|
||||
f'Requesting perfect answer for question: {item["question"]}'
|
||||
)
|
||||
|
||||
perfect_answers.append(
|
||||
await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
perfect_answer_messages,
|
||||
["answer"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"comment": "comment about answer quality", "overall": 0.0, '
|
||||
'"task_response": {"Fluency and Coherence": 0.0, "Lexical Resource": 0.0, '
|
||||
'"Grammatical Range and Accuracy": 0.0, "Pronunciation": 0.0}}')
|
||||
}
|
||||
]
|
||||
message = (
|
||||
f"Evaluate the given Speaking Part {task} response based on the IELTS grading system, ensuring a "
|
||||
"strict assessment that penalizes errors. Deduct points for deviations from the task, and "
|
||||
"assign a score of 0 if the response fails to address the question. Additionally, provide detailed "
|
||||
"commentary highlighting both strengths and weaknesses in the response."
|
||||
"\n\n The questions and answers are: \n\n'")
|
||||
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - Formatting answers and questions for prompt.'
|
||||
)
|
||||
|
||||
formatted_text = ""
|
||||
for i, entry in enumerate(answers, start=1):
|
||||
formatted_text += f"**Question {i}:**\n{entry['question']}\n\n"
|
||||
formatted_text += f"**Answer {i}:**\n{entry['answer']}\n\n"
|
||||
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - Formatted answers and questions for prompt: {formatted_text}'
|
||||
)
|
||||
|
||||
message += formatted_text
|
||||
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": message
|
||||
})
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Requesting grading of the answers.')
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO, messages, ["comment"], TemperatureSettings.GRADING_TEMPERATURE
|
||||
)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Answers graded: {str(response)}')
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Adding perfect answers to response.')
|
||||
|
||||
for i, answer in enumerate(perfect_answers, start=1):
|
||||
response['perfect_answer_' + str(i)] = answer
|
||||
|
||||
self._logger.info(
|
||||
f'POST - speaking_task_{task} - {str(request_id)} - Adding transcript and fixed texts to response.'
|
||||
)
|
||||
|
||||
for i, answer in enumerate(text_answers, start=1):
|
||||
response['transcript_' + str(i)] = answer
|
||||
response['fixed_text_' + str(i)] = await self._get_speaking_corrections(answer)
|
||||
|
||||
if response["overall"] == "0.0" or response["overall"] == 0.0:
|
||||
response["overall"] = self._calculate_overall(response)
|
||||
|
||||
self._logger.info(f'POST - speaking_task_{task} - {str(request_id)} - Final response: {str(response)}')
|
||||
|
||||
return response
|
||||
|
||||
# ==================================================================================================================
|
||||
# grade_speaking_task helpers
|
||||
# ==================================================================================================================
|
||||
|
||||
@staticmethod
|
||||
def _zero_rating(comment: str):
|
||||
return {
|
||||
"comment": comment,
|
||||
"overall": 0,
|
||||
"task_response": {
|
||||
"Fluency and Coherence": 0,
|
||||
"Lexical Resource": 0,
|
||||
"Grammatical Range and Accuracy": 0,
|
||||
"Pronunciation": 0
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_overall(response: Dict):
|
||||
return round(
|
||||
(
|
||||
response["task_response"]["Fluency and Coherence"] +
|
||||
response["task_response"]["Lexical Resource"] +
|
||||
response["task_response"]["Grammatical Range and Accuracy"] +
|
||||
response["task_response"]["Pronunciation"]
|
||||
) / 4, 1
|
||||
)
|
||||
|
||||
async def _get_speaking_corrections(self, text):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"fixed_text": "fixed transcription with no misspelling errors"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Fix the errors in the provided transcription and put it in a JSON. '
|
||||
f'Do not complete the answer, only replace what is wrong. \n The text: "{text}"'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
["fixed_text"],
|
||||
0.2,
|
||||
False
|
||||
)
|
||||
return response["fixed_text"]
|
||||
|
||||
async def create_videos_and_save_to_db(self, exercises, template, req_id):
|
||||
template = await self._create_video_per_part(exercises, template, 1)
|
||||
template = await self._create_video_per_part(exercises, template, 2)
|
||||
template = await self._create_video_per_part(exercises, template, 3)
|
||||
|
||||
await self._document_store.save_to_db_with_id("speaking", template, req_id)
|
||||
self._logger.info(f'Saved speaking to DB with id {req_id} : {str(template)}')
|
||||
|
||||
async def _create_video_per_part(self, exercises: List[Dict], template: Dict, part: int):
|
||||
template_index = part - 1
|
||||
|
||||
# Using list comprehension to find the element with the desired value in the 'type' field
|
||||
found_exercises = [element for element in exercises if element.get('type') == part]
|
||||
|
||||
# Check if any elements were found
|
||||
if found_exercises:
|
||||
exercise = found_exercises[0]
|
||||
self._logger.info(f'Creating video for speaking part {part}')
|
||||
if part in {1, 2}:
|
||||
result = await self._create_video(
|
||||
exercise["question"],
|
||||
(random.choice(list(AvatarEnum))).value,
|
||||
f'Failed to create video for part {part} question: {str(exercise["question"])}'
|
||||
)
|
||||
if result is not None:
|
||||
if part == 2:
|
||||
template["exercises"][template_index]["prompts"] = exercise["prompts"]
|
||||
|
||||
template["exercises"][template_index]["text"] = exercise["question"]
|
||||
template["exercises"][template_index]["title"] = exercise["topic"]
|
||||
template["exercises"][template_index]["video_url"] = result["video_url"]
|
||||
template["exercises"][template_index]["video_path"] = result["video_path"]
|
||||
else:
|
||||
questions = []
|
||||
for question in exercise["questions"]:
|
||||
result = await self._create_video(
|
||||
question,
|
||||
(random.choice(list(AvatarEnum))).value,
|
||||
f'Failed to create video for part {part} question: {str(exercise["question"])}'
|
||||
)
|
||||
if result is not None:
|
||||
video = {
|
||||
"text": question,
|
||||
"video_path": result["video_path"],
|
||||
"video_url": result["video_url"]
|
||||
}
|
||||
questions.append(video)
|
||||
|
||||
template["exercises"][template_index]["prompts"] = questions
|
||||
template["exercises"][template_index]["title"] = exercise["topic"]
|
||||
|
||||
if not found_exercises:
|
||||
template["exercises"].pop(template_index)
|
||||
|
||||
return template
|
||||
|
||||
# TODO: Check if it is intended to log the original question
|
||||
async def generate_speaking_video(self, original_question: str, topic: str, avatar: str, prompts: List[str]):
|
||||
if len(prompts) > 0:
|
||||
question = original_question + " In your answer you should consider: " + " ".join(prompts)
|
||||
else:
|
||||
question = original_question
|
||||
|
||||
error_msg = f'Failed to create video for part 1 question: {original_question}'
|
||||
|
||||
result = await self._create_video(
|
||||
question,
|
||||
avatar,
|
||||
error_msg
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
return {
|
||||
"text": original_question,
|
||||
"prompts": prompts,
|
||||
"title": topic,
|
||||
**result,
|
||||
"type": "speaking",
|
||||
"id": uuid.uuid4()
|
||||
}
|
||||
else:
|
||||
return str(error_msg)
|
||||
|
||||
async def generate_interactive_video(self, questions: List[str], avatar: str, topic: str):
|
||||
sp_questions = []
|
||||
self._logger.info('Creating videos for speaking part 3')
|
||||
for question in questions:
|
||||
result = await self._create_video(
|
||||
question,
|
||||
avatar,
|
||||
f'Failed to create video for part 3 question: {question}'
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
video = {
|
||||
"text": question,
|
||||
**result
|
||||
}
|
||||
sp_questions.append(video)
|
||||
|
||||
return {
|
||||
"prompts": sp_questions,
|
||||
"title": topic,
|
||||
"type": "interactiveSpeaking",
|
||||
"id": uuid.uuid4()
|
||||
}
|
||||
|
||||
async def _create_video(self, question: str, avatar: str, error_message: str):
|
||||
result = await self._vid_gen.create_video(question, avatar)
|
||||
if result is not None:
|
||||
sound_file_path = FilePaths.VIDEO_FILES_PATH + result
|
||||
firebase_file_path = FilePaths.FIREBASE_SPEAKING_VIDEO_FILES_PATH + result
|
||||
url = await self._file_storage.upload_file_firebase_get_url(firebase_file_path, sound_file_path)
|
||||
return {
|
||||
"video_path": firebase_file_path,
|
||||
"video_url": url
|
||||
}
|
||||
self._logger.error(error_message)
|
||||
return None
|
||||
13
app/services/impl/third_parties/__init__.py
Normal file
13
app/services/impl/third_parties/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .aws_polly import AWSPolly
|
||||
from .heygen import Heygen
|
||||
from .openai import OpenAI
|
||||
from .whisper import OpenAIWhisper
|
||||
from .gpt_zero import GPTZero
|
||||
|
||||
__all__ = [
|
||||
"AWSPolly",
|
||||
"Heygen",
|
||||
"OpenAI",
|
||||
"OpenAIWhisper",
|
||||
"GPTZero"
|
||||
]
|
||||
87
app/services/impl/third_parties/aws_polly.py
Normal file
87
app/services/impl/third_parties/aws_polly.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
import aiofiles
|
||||
from aiobotocore.client import BaseClient
|
||||
|
||||
from app.services.abc import ITextToSpeechService
|
||||
from app.configs.constants import NeuralVoices
|
||||
|
||||
|
||||
class AWSPolly(ITextToSpeechService):
|
||||
|
||||
def __init__(self, client: BaseClient):
|
||||
self._client = client
|
||||
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str = "neural", output_format: str = "mp3"):
|
||||
tts_response = await self._client.synthesize_speech(
|
||||
Engine=engine,
|
||||
Text=text,
|
||||
OutputFormat=output_format,
|
||||
VoiceId=voice
|
||||
)
|
||||
return await tts_response['AudioStream'].read()
|
||||
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
if isinstance(text, str):
|
||||
audio_segments = await self._text_to_speech(text)
|
||||
elif isinstance(text, list):
|
||||
audio_segments = await self._conversation_to_speech(text)
|
||||
else:
|
||||
raise ValueError("Unsupported argument for text_to_speech")
|
||||
|
||||
final_message = await self.synthesize_speech(
|
||||
"This audio recording, for the listening exercise, has finished.",
|
||||
"Stephen"
|
||||
)
|
||||
|
||||
# Add finish message
|
||||
audio_segments.append(final_message)
|
||||
|
||||
# Combine the audio segments into a single audio file
|
||||
combined_audio = b"".join(audio_segments)
|
||||
# Save the combined audio to a single file
|
||||
async with aiofiles.open(file_name, "wb") as f:
|
||||
await f.write(combined_audio)
|
||||
|
||||
print("Speech segments saved to " + file_name)
|
||||
|
||||
async def _text_to_speech(self, text: str):
|
||||
voice = random.choice(NeuralVoices.ALL_NEURAL_VOICES)['Id']
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
for part in self._divide_text(text):
|
||||
audio_segments.append(await self.synthesize_speech(part, voice))
|
||||
|
||||
return audio_segments
|
||||
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
# Iterate through the text segments, convert to audio segments, and store them
|
||||
for segment in conversation:
|
||||
audio_segments.append(await self.synthesize_speech(segment["text"], segment["voice"]))
|
||||
|
||||
return audio_segments
|
||||
|
||||
@staticmethod
|
||||
def _divide_text(text, max_length=3000):
|
||||
if len(text) <= max_length:
|
||||
return [text]
|
||||
|
||||
divisions = []
|
||||
current_position = 0
|
||||
|
||||
while current_position < len(text):
|
||||
next_position = min(current_position + max_length, len(text))
|
||||
next_period_position = text.rfind('.', current_position, next_position)
|
||||
|
||||
if next_period_position != -1 and next_period_position > current_position:
|
||||
divisions.append(text[current_position:next_period_position + 1])
|
||||
current_position = next_period_position + 1
|
||||
else:
|
||||
# If no '.' found in the next chunk, split at max_length
|
||||
divisions.append(text[current_position:next_position])
|
||||
current_position = next_position
|
||||
|
||||
return divisions
|
||||
52
app/services/impl/third_parties/gpt_zero.py
Normal file
52
app/services/impl/third_parties/gpt_zero.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from logging import getLogger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.services.abc.third_parties.ai_detector import IAIDetectorService
|
||||
|
||||
|
||||
class GPTZero(IAIDetectorService):
|
||||
|
||||
_GPT_ZERO_ENDPOINT = 'https://api.gptzero.me/v2/predict/text'
|
||||
|
||||
def __init__(self, client: AsyncClient, gpt_zero_key: str):
|
||||
self._header = {
|
||||
'x-api-key': gpt_zero_key
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
async def run_detection(self, text: str):
|
||||
data = {
|
||||
'document': text,
|
||||
'version': '',
|
||||
'multilingual': False
|
||||
}
|
||||
|
||||
response = await self._http_client.post(self._GPT_ZERO_ENDPOINT, headers=self._header, json=data)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return self._parse_detection(response.json())
|
||||
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
try:
|
||||
text_scan = response["documents"][0]
|
||||
|
||||
filtered_sentences = [
|
||||
{
|
||||
"sentence": item["sentence"],
|
||||
"highlight_sentence_for_ai": item["highlight_sentence_for_ai"]
|
||||
}
|
||||
for item in text_scan["sentences"]
|
||||
]
|
||||
|
||||
return {
|
||||
"class_probabilities": text_scan["class_probabilities"],
|
||||
"confidence_category": text_scan["confidence_category"],
|
||||
"predicted_class": text_scan["predicted_class"],
|
||||
"sentences": filtered_sentences
|
||||
}
|
||||
except Exception as e:
|
||||
self._logger.error(f'Failed to parse GPT\'s Zero response: {str(e)}')
|
||||
return None
|
||||
90
app/services/impl/third_parties/heygen.py
Normal file
90
app/services/impl/third_parties/heygen.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import aiofiles
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.services.abc import IVideoGeneratorService
|
||||
|
||||
|
||||
class Heygen(IVideoGeneratorService):
|
||||
|
||||
# TODO: Not used, remove if not necessary
|
||||
# CREATE_VIDEO_URL = 'https://api.heygen.com/v1/template.generate'
|
||||
|
||||
_GET_VIDEO_URL = 'https://api.heygen.com/v1/video_status.get'
|
||||
|
||||
def __init__(self, client: AsyncClient, heygen_token: str):
|
||||
self._get_header = {
|
||||
'X-Api-Key': heygen_token
|
||||
}
|
||||
self._post_header = {
|
||||
'X-Api-Key': heygen_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
# POST TO CREATE VIDEO
|
||||
create_video_url = 'https://api.heygen.com/v2/template/' + avatar + '/generate'
|
||||
data = {
|
||||
"test": False,
|
||||
"caption": False,
|
||||
"title": "video_title",
|
||||
"variables": {
|
||||
"script_here": {
|
||||
"name": "script_here",
|
||||
"type": "text",
|
||||
"properties": {
|
||||
"content": text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response = await self._http_client.post(create_video_url, headers=self._post_header, json=data)
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# GET TO CHECK STATUS AND GET VIDEO WHEN READY
|
||||
video_id = response.json()["data"]["video_id"]
|
||||
params = {
|
||||
'video_id': response.json()["data"]["video_id"]
|
||||
}
|
||||
response = {}
|
||||
status = "processing"
|
||||
error = None
|
||||
|
||||
while status != "completed" and error is None:
|
||||
response = await self._http_client.get(self._GET_VIDEO_URL, headers=self._get_header, params=params)
|
||||
response_data = response.json()
|
||||
|
||||
status = response_data["data"]["status"]
|
||||
error = response_data["data"]["error"]
|
||||
|
||||
if status != "completed" and error is None:
|
||||
self._logger.info(f"Status: {status}")
|
||||
await asyncio.sleep(10) # Wait for 10 second before the next request
|
||||
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# DOWNLOAD VIDEO
|
||||
download_url = response.json()['data']['video_url']
|
||||
output_directory = 'download-video/'
|
||||
output_filename = video_id + '.mp4'
|
||||
|
||||
response = await self._http_client.get(download_url)
|
||||
|
||||
if response.status_code == 200:
|
||||
os.makedirs(output_directory, exist_ok=True) # Create the directory if it doesn't exist
|
||||
output_path = os.path.join(output_directory, output_filename)
|
||||
async with aiofiles.open(output_path, 'wb') as f:
|
||||
await f.write(response.content)
|
||||
self._logger.info(f"File '{output_filename}' downloaded successfully.")
|
||||
return output_filename
|
||||
else:
|
||||
self._logger.error(f"Failed to download file. Status code: {response.status_code}")
|
||||
return None
|
||||
|
||||
97
app/services/impl/third_parties/openai.py
Normal file
97
app/services/impl/third_parties/openai.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
|
||||
from app.services.abc import ILLMService
|
||||
from app.helpers import count_tokens
|
||||
from app.configs.constants import BLACKLISTED_WORDS
|
||||
|
||||
|
||||
class OpenAI(ILLMService):
|
||||
|
||||
MAX_TOKENS = 4097
|
||||
TRY_LIMIT = 2
|
||||
|
||||
def __init__(self, client: AsyncOpenAI):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
if token_count == -1:
|
||||
token_count = self._count_total_tokens(messages)
|
||||
return await self._prediction(model, messages, token_count, fields_to_check, temperature, 0, check_blacklisted)
|
||||
|
||||
async def _prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
token_count: int,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
try_count: int,
|
||||
check_blacklisted: bool,
|
||||
):
|
||||
result = await self._client.chat.completions.create(
|
||||
model=model,
|
||||
max_tokens=int(self.MAX_TOKENS - token_count - 300),
|
||||
temperature=float(temperature),
|
||||
messages=messages,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
result = result.choices[0].message.content
|
||||
|
||||
if check_blacklisted:
|
||||
found_blacklisted_word = self._get_found_blacklisted_words(result)
|
||||
|
||||
if found_blacklisted_word is not None and try_count < self.TRY_LIMIT:
|
||||
self._logger.warning("Result contains blacklisted words: " + str(found_blacklisted_word))
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
elif found_blacklisted_word is not None and try_count >= self.TRY_LIMIT:
|
||||
return ""
|
||||
|
||||
if fields_to_check is None:
|
||||
return json.loads(result)
|
||||
|
||||
if not self._check_fields(result, fields_to_check) and try_count < self.TRY_LIMIT:
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
|
||||
return json.loads(result)
|
||||
|
||||
async def prediction_override(self, **kwargs):
|
||||
return await self._client.chat.completions.create(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_found_blacklisted_words(text: str):
|
||||
text_lower = text.lower()
|
||||
for word in BLACKLISTED_WORDS:
|
||||
if re.search(r'\b' + re.escape(word) + r'\b', text_lower):
|
||||
return word
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _count_total_tokens(messages):
|
||||
total_tokens = 0
|
||||
for message in messages:
|
||||
total_tokens += count_tokens(message["content"])["n_tokens"]
|
||||
return total_tokens
|
||||
|
||||
@staticmethod
|
||||
def _check_fields(obj, fields):
|
||||
return all(field in obj for field in fields)
|
||||
22
app/services/impl/third_parties/whisper.py
Normal file
22
app/services/impl/third_parties/whisper.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from whisper import Whisper
|
||||
from app.services.abc import ISpeechToTextService
|
||||
|
||||
|
||||
class OpenAIWhisper(ISpeechToTextService):
|
||||
|
||||
def __init__(self, model: Whisper):
|
||||
self._model = model
|
||||
|
||||
async def speech_to_text(self, file_path):
|
||||
if os.path.exists(file_path):
|
||||
result = await run_in_threadpool(
|
||||
self._model.transcribe, file_path, fp16=False, language='English', verbose=False
|
||||
)
|
||||
return result["text"]
|
||||
else:
|
||||
print("File not found:", file_path)
|
||||
raise Exception("File " + file_path + " not found.")
|
||||
68
app/services/impl/training.py
Normal file
68
app/services/impl/training.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import re
|
||||
from functools import reduce
|
||||
|
||||
from app.configs.constants import TemperatureSettings, GPTModels
|
||||
from app.helpers import count_tokens
|
||||
from app.services.abc import ILLMService, ITrainingService
|
||||
|
||||
|
||||
class TrainingService(ITrainingService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
messages = self._get_question_tips(question, answer, correct_answer, context)
|
||||
|
||||
token_count = reduce(lambda count, item: count + count_tokens(item)['n_tokens'],
|
||||
map(lambda x: x["content"], filter(lambda x: "content" in x, messages)), 0)
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
None,
|
||||
TemperatureSettings.TIPS_TEMPERATURE,
|
||||
token_count=token_count
|
||||
)
|
||||
|
||||
if isinstance(response, str):
|
||||
response = re.sub(r"^[a-zA-Z0-9_]+\:\s*", "", response)
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _get_question_tips(question: str, answer: str, correct_answer: str, context: str = None):
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"You are a IELTS exam program that analyzes incorrect answers to questions and gives tips to "
|
||||
"help students understand why it was a wrong answer and gives helpful insight for the future. "
|
||||
"The tip should refer to the context and question."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
if not (context is None or context == ""):
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"This is the context for the question: {context}",
|
||||
})
|
||||
|
||||
messages.extend([
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the question: {question}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the answer: {answer}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the correct answer: {correct_answer}",
|
||||
}
|
||||
])
|
||||
|
||||
return messages
|
||||
|
||||
147
app/services/impl/writing.py
Normal file
147
app/services/impl/writing.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from app.services.abc import IWritingService, ILLMService, IAIDetectorService
|
||||
from app.configs.constants import GPTModels, TemperatureSettings
|
||||
from app.helpers import TextHelper, ExercisesHelper
|
||||
|
||||
|
||||
class WritingService(IWritingService):
|
||||
|
||||
def __init__(self, llm: ILLMService, ai_detector: IAIDetectorService):
|
||||
self._llm = llm
|
||||
self._ai_detector = ai_detector
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": self._get_writing_prompt(task, topic, difficulty)
|
||||
}
|
||||
]
|
||||
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["prompt"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return {
|
||||
"question": response["prompt"].strip(),
|
||||
"difficulty": difficulty,
|
||||
"topic": topic
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_writing_prompt(task: int, topic: str, difficulty: str):
|
||||
return (
|
||||
'Craft a prompt for an IELTS Writing Task 1 General Training exercise that instructs the '
|
||||
'student to compose a letter. The prompt should present a specific scenario or situation, '
|
||||
f'based on the topic of "{topic}", requiring the student to provide information, '
|
||||
'advice, or instructions within the letter. Make sure that the generated prompt is '
|
||||
f'of {difficulty} difficulty and does not contain forbidden subjects in muslim countries.'
|
||||
) if task == 1 else (
|
||||
f'Craft a comprehensive question of {difficulty} difficulty like the ones for IELTS '
|
||||
'Writing Task 2 General Training that directs the candidate to delve into an in-depth '
|
||||
f'analysis of contrasting perspectives on the topic of "{topic}".'
|
||||
)
|
||||
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
bare_minimum = 100 if task == 1 else 180
|
||||
minimum = 150 if task == 1 else 250
|
||||
|
||||
# TODO: left as is, don't know if this is intended or not
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
temperature = (
|
||||
TemperatureSettings.GRADING_TEMPERATURE
|
||||
if task == 1 else
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if not TextHelper.has_words(answer):
|
||||
return self._zero_rating("The answer does not contain enough english words.")
|
||||
elif not TextHelper.has_x_words(answer, bare_minimum):
|
||||
return self._zero_rating("The answer is insufficient and too small to be graded.")
|
||||
else:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"perfect_answer": "example perfect answer", "comment": '
|
||||
'"comment about answer quality", "overall": 0.0, "task_response": '
|
||||
'{"Task Achievement": 0.0, "Coherence and Cohesion": 0.0, '
|
||||
'"Lexical Resource": 0.0, "Grammatical Range and Accuracy": 0.0 }'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Evaluate the given Writing Task {task} response based on the IELTS grading system, '
|
||||
'ensuring a strict assessment that penalizes errors. Deduct points for deviations '
|
||||
'from the task, and assign a score of 0 if the response fails to address the question. '
|
||||
f'Additionally, provide an exemplary answer with a minimum of {minimum} words, along with a '
|
||||
'detailed commentary highlighting both strengths and weaknesses in the response. '
|
||||
f'\n Question: "{question}" \n Answer: "{answer}"')
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'The perfect answer must have at least {minimum} words.'
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["comment"],
|
||||
temperature
|
||||
)
|
||||
|
||||
response["overall"] = ExercisesHelper.fix_writing_overall(response["overall"], response["task_response"])
|
||||
response['fixed_text'] = await self._get_fixed_text(answer)
|
||||
|
||||
ai_detection = await self._ai_detector.run_detection(answer)
|
||||
if ai_detection is not None:
|
||||
response['ai_detection'] = ai_detection
|
||||
|
||||
return response
|
||||
|
||||
async def _get_fixed_text(self, text):
|
||||
messages = [
|
||||
{"role": "system", "content": ('You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"fixed_text": "fixed test with no misspelling errors"}')
|
||||
},
|
||||
{"role": "user", "content": (
|
||||
'Fix the errors in the given text and put it in a JSON. '
|
||||
f'Do not complete the answer, only replace what is wrong. \n The text: "{text}"')
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
["fixed_text"],
|
||||
0.2,
|
||||
False
|
||||
)
|
||||
return response["fixed_text"]
|
||||
|
||||
@staticmethod
|
||||
def _zero_rating(comment: str):
|
||||
return {
|
||||
'comment': comment,
|
||||
'overall': 0,
|
||||
'task_response': {
|
||||
'Coherence and Cohesion': 0,
|
||||
'Grammatical Range and Accuracy': 0,
|
||||
'Lexical Resource': 0,
|
||||
'Task Achievement': 0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user