Compare commits
319 Commits
feature/qu
...
26ad153f7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ad153f7c | ||
|
|
d40eac7080 | ||
|
|
8550b520e1 | ||
|
|
acce3f11b2 | ||
|
|
fb73213d63 | ||
|
|
b4d4afd83a | ||
|
|
4fc58523bc | ||
|
|
f0453d06c7 | ||
|
|
984ecbb824 | ||
|
|
9bfad2d47f | ||
|
|
7d04b144c4 | ||
|
|
22593b737b | ||
|
|
bf77629ddf | ||
|
|
09d6242360 | ||
|
|
113fef0404 | ||
|
|
0262971b11 | ||
|
|
4b6a7ce54e | ||
|
|
05a2806daa | ||
|
|
a048269dfd | ||
|
|
a8f8b37e40 | ||
|
|
e076eaaeb7 | ||
|
|
d45d851e53 | ||
|
|
6111202049 | ||
|
|
fa028aa0e7 | ||
|
|
196f9e9c3e | ||
|
|
0222c339fe | ||
|
|
a9a5e17b24 | ||
|
|
6982068864 | ||
|
|
06471e9fab | ||
|
|
d64cb929c7 | ||
|
|
68cab80851 | ||
|
|
4e05c4d913 | ||
|
|
12376d422d | ||
|
|
1603fa4ee6 | ||
|
|
93d9b700fd | ||
|
|
a2d1133915 | ||
|
|
6681b2d0e9 | ||
|
|
54a01f9631 | ||
|
|
72d2c0121a | ||
|
|
47cdfe1478 | ||
|
|
6e0276b79d | ||
|
|
a7da187ec6 | ||
|
|
c74b2b9b7b | ||
|
|
93044203f6 | ||
|
|
a54dfad43a | ||
|
|
18103c931e | ||
|
|
d04759d979 | ||
|
|
35ec00504b | ||
|
|
e99eda485e | ||
|
|
229dbe3e29 | ||
|
|
362d018a05 | ||
|
|
e69925fd53 | ||
|
|
6daab0d9a7 | ||
|
|
1c32093ade | ||
|
|
12b3c45173 | ||
|
|
684e07e2df | ||
|
|
1d11552836 | ||
|
|
afeaf118c6 | ||
|
|
6909d75eb6 | ||
|
|
cf1b676312 | ||
|
|
e955a16abf | ||
|
|
09998478d1 | ||
|
|
b473b30a75 | ||
|
|
a3ea91793e | ||
|
|
81a74c5f3b | ||
|
|
8d95aa6c21 | ||
|
|
136309120b | ||
|
|
2263c55776 | ||
|
|
fd7aa4bd55 | ||
|
|
dc16749256 | ||
|
|
f600781547 | ||
|
|
c9e293fb11 | ||
|
|
323ef629d4 | ||
|
|
a2e96f8e54 | ||
|
|
e51cd891d2 | ||
|
|
f02a34fda2 | ||
|
|
dc04fdf74c | ||
|
|
8233498f51 | ||
|
|
98565f3468 | ||
|
|
84ed2f2f6a | ||
|
|
95962f9bce | ||
|
|
8720c590e0 | ||
|
|
163e4cf42d | ||
|
|
2a032c5aba | ||
|
|
5289f33599 | ||
|
|
164f47994b | ||
|
|
895aaa1b33 | ||
|
|
aa1433e9ea | ||
|
|
111108556b | ||
|
|
8eb5fb6d5f | ||
|
|
c004d9c83c | ||
|
|
66abc42abb | ||
|
|
2b59119eca | ||
|
|
b9a35281ec | ||
|
|
2bbc1f456d | ||
|
|
e8ec862f86 | ||
|
|
8d4584b8b7 | ||
|
|
7a0424aa33 | ||
|
|
24ce198dfd | ||
|
|
81911e635c | ||
|
|
849db06760 | ||
|
|
6a38164f9b | ||
|
|
8ae9b64f1a | ||
|
|
676f660f3e | ||
|
|
ddf050d692 | ||
|
|
6cb7c07f57 | ||
|
|
8c60f4596f | ||
|
|
cd11fa38ae | ||
|
|
a328f01d2e | ||
|
|
a931c5ec2e | ||
|
|
bfc9565e85 | ||
|
|
3d70bcbfd1 | ||
|
|
a2cfa335d7 | ||
|
|
0427d6e1b4 | ||
|
|
31c6ed570a | ||
|
|
3a27c42a69 | ||
|
|
260dba1ee6 | ||
|
|
a88d6bb568 | ||
|
|
f0f904f2e4 | ||
|
|
a23bbe581a | ||
|
|
bb26282d25 | ||
|
|
73c29cda25 | ||
|
|
aaa3361575 | ||
|
|
94a16b636d | ||
|
|
cffec795a7 | ||
|
|
b2b4dfb74e | ||
|
|
2716f52a0a | ||
|
|
4099d99f80 | ||
|
|
ab4db36445 | ||
|
|
59f047afba | ||
|
|
09b57cb346 | ||
|
|
bfc3e3f083 | ||
|
|
7b5e10fd79 | ||
|
|
a2a160f61b | ||
|
|
f92a803d96 | ||
|
|
5d5cd21e1e | ||
|
|
06a8384f42 | ||
|
|
dd74a3d259 | ||
|
|
efff0b904e | ||
|
|
cf7a966141 | ||
|
|
03f5b7d72c | ||
|
|
d68617f33b | ||
|
|
eeaa04f856 | ||
|
|
beccf8b501 | ||
|
|
470f4cc83b | ||
|
|
3ad411ed71 | ||
|
|
7144a3f3ca | ||
|
|
b795a3fb79 | ||
|
|
034be25e8e | ||
|
|
a931f06c47 | ||
|
|
8e56a3228b | ||
|
|
14c5914420 | ||
|
|
6878e0a276 | ||
|
|
1f29ac6ee5 | ||
|
|
a1ee7e47da | ||
|
|
adfc027458 | ||
|
|
3a7bb7764f | ||
|
|
19f204d74d | ||
|
|
88ba9ab561 | ||
|
|
34afb5d1e8 | ||
|
|
eb904f836a | ||
|
|
ca12ad1161 | ||
|
|
8b8460517c | ||
|
|
9be9bfce0e | ||
|
|
4776f24229 | ||
|
|
3cf9fa5cba | ||
|
|
bf9251eebb | ||
|
|
1ecda04c6b | ||
|
|
d5621c1793 | ||
|
|
4c41942dfe | ||
|
|
bef606fe14 | ||
|
|
358f240d16 | ||
|
|
e7d84b9704 | ||
|
|
b4dc6be927 | ||
|
|
afca610c09 | ||
|
|
495502bc93 | ||
|
|
565874ad41 | ||
|
|
e693f5ee2a | ||
|
|
a8b46160d4 | ||
|
|
640039d372 | ||
|
|
a3cd1cdf59 | ||
|
|
9a696bbeb5 | ||
|
|
2adb7d1847 | ||
|
|
b93ead3a7b | ||
|
|
ad3a32ce45 | ||
|
|
ee5f23b3d7 | ||
|
|
545aee1a19 | ||
|
|
3f749f1ff5 | ||
|
|
32ac2149f5 | ||
|
|
64cc207fe8 | ||
|
|
a4caecdb4f | ||
|
|
20dfd5be78 | ||
|
|
1d110d5fa9 | ||
|
|
7633822916 | ||
|
|
9bc06d8340 | ||
|
|
4ff3b02a1d | ||
|
|
7637322239 | ||
|
|
3676d7ad39 | ||
|
|
b7c18517de | ||
|
|
fe753fe72c | ||
|
|
a0a193844d | ||
|
|
9654d9ff64 | ||
|
|
e568aff4e4 | ||
|
|
070e8808b1 | ||
|
|
c77f7178ae | ||
|
|
5f7fe23afd | ||
|
|
ca93129082 | ||
|
|
6e2355ee4c | ||
|
|
f1d2ec3bf8 | ||
|
|
08f05ac3e0 | ||
|
|
8ec72ff539 | ||
|
|
373867d520 | ||
|
|
894cabdeb0 | ||
|
|
94c2b5a052 | ||
|
|
3aa33f10b4 | ||
|
|
cc3371c597 | ||
|
|
7049fd86d4 | ||
|
|
6aba83f3bb | ||
|
|
73532d5fed | ||
|
|
f02d113f40 | ||
|
|
6e65732e94 | ||
|
|
bed07ca819 | ||
|
|
274bd79c6a | ||
|
|
8b83a4163d | ||
|
|
1bd012d340 | ||
|
|
a200b29dba | ||
|
|
f3f9415665 | ||
|
|
d4694e55bf | ||
|
|
b46f6011d3 | ||
|
|
d532f7deb4 | ||
|
|
9149e4b197 | ||
|
|
3a1dc33e1b | ||
|
|
678ef4b6c0 | ||
|
|
fcf2993de0 | ||
|
|
45a4dbe018 | ||
|
|
81d7167cbf | ||
|
|
1c888f22e2 | ||
|
|
7bbb03e4b2 | ||
|
|
97f30ea881 | ||
|
|
ad2e7a6322 | ||
|
|
bc2cedb821 | ||
|
|
64a4759fbc | ||
|
|
54950e11d2 | ||
|
|
ac7ba2edfa | ||
|
|
a577eed013 | ||
|
|
6c03e3590c | ||
|
|
1591f8d9fb | ||
|
|
92c92dfd98 | ||
|
|
ccc606d5de | ||
|
|
d8da4d0348 | ||
|
|
de4042efac | ||
|
|
5aedd1864d | ||
|
|
555d5e55b0 | ||
|
|
61f876b3e4 | ||
|
|
a40ce04ad2 | ||
|
|
6baf669216 | ||
|
|
e7a96c6880 | ||
|
|
75df686cd1 | ||
|
|
046606a8ec | ||
|
|
efef92343a | ||
|
|
ac27239787 | ||
|
|
f2e8497756 | ||
|
|
63823a01de | ||
|
|
9b3997f65e | ||
|
|
479620116d | ||
|
|
2b91cfe26d | ||
|
|
9f4aed52ae | ||
|
|
50c39e5f9c | ||
|
|
57d6e7ffde | ||
|
|
171d72109e | ||
|
|
34154b1e5f | ||
|
|
760fe27411 | ||
|
|
4a0ae88fed | ||
|
|
869e74f384 | ||
|
|
22de63c346 | ||
|
|
05202c5cf0 | ||
|
|
70e442a97e | ||
|
|
362c9f4737 | ||
|
|
0bcf362b3f | ||
|
|
73324909f6 | ||
|
|
0684314cef | ||
|
|
223a7dfd11 | ||
|
|
75985a4077 | ||
|
|
d1b8793885 | ||
|
|
589909cd3c | ||
|
|
d6a008b353 | ||
|
|
695d9b589a | ||
|
|
274252bf92 | ||
|
|
c3957403f6 | ||
|
|
6416582ee0 | ||
|
|
d6b75de856 | ||
|
|
51085619ee | ||
|
|
e7eb7c96ba | ||
|
|
035939e9a7 | ||
|
|
8d9cd2949c | ||
|
|
f77fafa864 | ||
|
|
8f9b65281e | ||
|
|
680ec00885 | ||
|
|
c275cb887d | ||
|
|
00375489e8 | ||
|
|
8e043104ad | ||
|
|
eb6e9b4ef7 | ||
|
|
64776617f2 | ||
|
|
685fde0b77 | ||
|
|
cfff3ee6dd | ||
|
|
fcd7483fd9 | ||
|
|
a31489d850 | ||
|
|
ca6094c3e7 | ||
|
|
18bf6d59e0 | ||
|
|
df30edd45e | ||
|
|
7d08cd9608 | ||
|
|
a67e7a4abc | ||
|
|
0a53f2c1b8 | ||
|
|
0b661fe108 | ||
|
|
7a1dbb76de | ||
|
|
a784400568 | ||
|
|
55ae1b28c7 | ||
|
|
f0b85fa500 | ||
|
|
4e1ad6dc67 |
@@ -5,3 +5,6 @@ README.md
|
||||
*.pyd
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
postman
|
||||
/scripts
|
||||
/.venv
|
||||
3
.env
3
.env
@@ -1,3 +0,0 @@
|
||||
OPENAI_API_KEY=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
|
||||
JWT_SECRET_KEY=6e9c124ba92e8814719dcb0f21200c8aa4d0f119a994ac5e06eb90a366c83ab2
|
||||
JWT_TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0In0.Emrs2D3BmMP4b3zMjw0fJTPeyMwWEBDbxx2vvaWguO0
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
__pycache__
|
||||
.idea
|
||||
.idea
|
||||
.env
|
||||
.DS_Store
|
||||
.venv
|
||||
_scripts
|
||||
*.env
|
||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
21
.idea/ielts-be.iml
generated
21
.idea/ielts-be.iml
generated
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/../flaskProject\templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ielts-be.iml" filepath="$PROJECT_DIR$/.idea/ielts-be.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,6 +1,12 @@
|
||||
FROM python:3.11-slim as requirements-stage
|
||||
WORKDIR /tmp
|
||||
RUN pip install poetry
|
||||
COPY pyproject.toml ./poetry.lock* /tmp/
|
||||
# https://python-poetry.org/docs/cli#export
|
||||
RUN poetry self add poetry-plugin-export
|
||||
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||
|
||||
|
||||
# Use the official lightweight Python image.
|
||||
# https://hub.docker.com/_/python
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Allow statements and log messages to immediately appear in the logs
|
||||
@@ -9,16 +15,35 @@ ENV PYTHONUNBUFFERED True
|
||||
# Copy local code to the container image.
|
||||
ENV APP_HOME /app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Install production dependencies.
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY --from=requirements-stage /tmp/requirements.txt /app/requirements.txt
|
||||
|
||||
EXPOSE 5000
|
||||
RUN apt update && apt install -y \
|
||||
ffmpeg \
|
||||
poppler-utils \
|
||||
texlive-latex-base \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-xetex \
|
||||
pandoc \
|
||||
librsvg2-bin \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN npm install -g firebase-tools
|
||||
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the web service on container startup. Here we use the gunicorn
|
||||
# webserver, with one worker process and 8 threads.
|
||||
# For environments with multiple CPU cores, increase the number of workers
|
||||
# to be equal to the cores available.
|
||||
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
|
||||
CMD exec gunicorn --bind 0.0.0.0:5000 --workers 1 --threads 8 --timeout 0 app:app
|
||||
ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "--timeout", "0", "-k", "uvicorn.workers.UvicornWorker", "ielts_be:app"]
|
||||
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Run the app
|
||||
|
||||
1. pip install poetry
|
||||
2. poetry install
|
||||
3. python app.py
|
||||
|
||||
# Modules
|
||||
|
||||
- api -> endpoints
|
||||
- configs -> app configs and constants
|
||||
- controllers -> meant for handling exceptions, transforming data or orchestrate complex use cases with several services, for now mostly just calls services directly
|
||||
- dtos -> pydantic models used for receiving data and for validation
|
||||
- exceptions -> if custom exceptions are needed to throw in services so they can be handled in the controllers to construct some specific http response
|
||||
- helpers -> a bunch of lightweight functions grouped by some kind of logic
|
||||
- mappers -> to map complex data
|
||||
- middlewares -> classes that are run before executing the endpoint code
|
||||
- repositories -> interfaces with data stores
|
||||
- services -> all the business logic goes here
|
||||
- utils -> loose functions used on one-off occasions
|
||||
|
||||
# Dependency injection
|
||||
|
||||
If you want to add new controllers/services/repositories you will have to change
|
||||
app/configs/dependency_injection.py
|
||||
|
||||
Also make sure you have @inject on your endpoint when calling these.
|
||||
116
app.py
116
app.py
@@ -1,105 +1,25 @@
|
||||
from flask import Flask, request
|
||||
from flask_jwt_extended import JWTManager, jwt_required
|
||||
from functools import reduce
|
||||
from helper.token_counter import count_tokens
|
||||
from helper.openai_interface import make_openai_call
|
||||
import os
|
||||
|
||||
import click
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['JWT_SECRET_KEY'] = os.getenv("JWT_SECRET_KEY")
|
||||
jwt = JWTManager(app)
|
||||
|
||||
GRADING_TEMPERATURE = 0.1
|
||||
GEN_QUESTION_TEMPERATURE = 0.7
|
||||
WRITING_TASK_2_POST_FIELDS = ['overall', 'comment', 'task_response']
|
||||
WRITING_TASK_2_GET_FIELDS = ['question']
|
||||
|
||||
@app.route('/writing_task2', methods=['POST'])
|
||||
@jwt_required()
|
||||
def grade_writing_task():
|
||||
data = request.get_json() # Assuming the request data is in JSON format
|
||||
question = data.get('question')
|
||||
answer = data.get('answer')
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a IELTS examiner.",
|
||||
},
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"The question you have to grade is of type Writing Task 2 and is the following: {question}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "It is mandatory for you to provide your response with the overall grade and breakdown grades, "
|
||||
"in the following json format: {'comment': 'comment about answer quality', 'overall': 7.0, 'task_response': {'Task Achievement': 8.0, "
|
||||
"'Coherence and Cohesion': 6.5, 'Lexical Resource': 7.5, 'Grammatical Range and Accuracy': "
|
||||
"6.0}}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Example output: { 'comment': 'Overall, the response is good but there are some areas that need "
|
||||
"improvement.\n\nIn terms of Task Achievement, the writer has addressed all parts of the question "
|
||||
"and has provided a clear opinion on the topic. However, some of the points made are not fully "
|
||||
"developed or supported with examples.\n\nIn terms of Coherence and Cohesion, there is a clear "
|
||||
"structure to the response with an introduction, body paragraphs and conclusion. However, there "
|
||||
"are some issues with cohesion as some sentences do not flow smoothly from one to another.\n\nIn "
|
||||
"terms of Lexical Resource, there is a good range of vocabulary used throughout the response and "
|
||||
"some less common words have been used effectively.\n\nIn terms of Grammatical Range and Accuracy, "
|
||||
"there are some errors in grammar and sentence structure which affect clarity in places.\n\nOverall, "
|
||||
"this response would score a band 6.5.', 'overall': 6.5, 'task_response': "
|
||||
"{ 'Coherence and Cohesion': 6.5, 'Grammatical Range and Accuracy': 6.0, 'Lexical Resource': 7.0, "
|
||||
"'Task Achievement': 7.0}}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Evaluate this answer according to ielts grading system: {answer}",
|
||||
},
|
||||
]
|
||||
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 = make_openai_call(messages, token_count, WRITING_TASK_2_POST_FIELDS, GRADING_TEMPERATURE)
|
||||
return response
|
||||
|
||||
@app.route('/writing_task2', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_writing_task_question():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a IELTS program that generates questions for the exams.",
|
||||
},
|
||||
{
|
||||
"role": "system",
|
||||
"content": "The question you have to generate is of type Writing Task 2 and is the following.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "It is mandatory for you to provide your response with the question "
|
||||
"in the following json format: {'question': 'question'}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Example output: { 'question': 'We are becoming increasingly dependent on computers. "
|
||||
"They are used in businesses, hospitals, crime detection and even to fly planes. What things will "
|
||||
"they be used for in the future? Is this dependence on computers a good thing or should we he more "
|
||||
"auspicious of their benefits?'}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Generate a question for IELTS exam Writing Task 2.",
|
||||
},
|
||||
]
|
||||
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 = make_openai_call(messages, token_count, WRITING_TASK_2_GET_FIELDS, GEN_QUESTION_TEMPERATURE)
|
||||
return response
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--env",
|
||||
type=click.Choice(["local", "staging", "production"], case_sensitive=False),
|
||||
default="staging",
|
||||
)
|
||||
def main(env: str):
|
||||
uvicorn.run(
|
||||
app="ielts_be:app",
|
||||
host="localhost",
|
||||
port=8000,
|
||||
reload=True if env != "production" else False,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,10 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
ielts-be:
|
||||
container_name: ielts-be
|
||||
build: .
|
||||
image: ecrop/ielts-be:latest
|
||||
ports:
|
||||
- 8080:5000
|
||||
restart: unless-stopped
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
ielts-be:
|
||||
container_name: ielts-be
|
||||
build: .
|
||||
image: ecrop/ielts-be:latest
|
||||
ports:
|
||||
- 8080:8000
|
||||
restart: unless-stopped
|
||||
|
||||
62
elai/AvatarEnum.py
Normal file
62
elai/AvatarEnum.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AvatarEnum(Enum):
|
||||
# Works
|
||||
GIA_BUSINESS = {
|
||||
"avatar_code": "gia.business",
|
||||
"avatar_gender": "female",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
|
||||
"avatar_canvas": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
|
||||
"voice_id": "EXAVITQu4vr4xnSDxMaL",
|
||||
"voice_provider": "elevenlabs"
|
||||
}
|
||||
# Works
|
||||
VADIM_BUSINESS = {
|
||||
"avatar_code": "vadim.business",
|
||||
"avatar_gender": "male",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/vadim/business/vadim_business.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/vadim/business/vadim_business.png",
|
||||
"voice_id": "flq6f7yk4E4fJM5XTYuZ",
|
||||
"voice_provider": "elevenlabs"
|
||||
}
|
||||
ORHAN_BUSINESS = {
|
||||
"avatar_code": "orhan.business",
|
||||
"avatar_gender": "male",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/orhan/business/orhan.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/orhan/business/orhan.png",
|
||||
"voice_id": "en-US-AndrewMultilingualNeural",
|
||||
"voice_provider": "azure"
|
||||
}
|
||||
FLORA_BUSINESS = {
|
||||
"avatar_code": "flora.business",
|
||||
"avatar_gender": "female",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/flora/business/flora_business.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/flora/business/flora_business.png",
|
||||
"voice_id": "en-US-JaneNeural",
|
||||
"voice_provider": "azure"
|
||||
}
|
||||
SCARLETT_BUSINESS = {
|
||||
"avatar_code": "scarlett.business",
|
||||
"avatar_gender": "female",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/scarlett/business/scarlett_business.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/scarlett/business/scarlett_business.png",
|
||||
"voice_id": "en-US-NancyNeural",
|
||||
"voice_provider": "azure"
|
||||
}
|
||||
PARKER_CASUAL = {
|
||||
"avatar_code": "parker.casual",
|
||||
"avatar_gender": "male",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/parker/casual/parker_casual.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/parker/casual/parker_casual.png",
|
||||
"voice_id": "en-US-TonyNeural",
|
||||
"voice_provider": "azure"
|
||||
}
|
||||
ETHAN_BUSINESS = {
|
||||
"avatar_code": "ethan.business",
|
||||
"avatar_gender": "male",
|
||||
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/ethan/business/ethan_business_low.png",
|
||||
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/ethan/business/ethan_business_low.png",
|
||||
"voice_id": "en-US-JasonNeural",
|
||||
"voice_provider": "azure"
|
||||
}
|
||||
1965
elai/avatars.json
Normal file
1965
elai/avatars.json
Normal file
File diff suppressed because it is too large
Load Diff
3386
elai/english_voices.json
Normal file
3386
elai/english_voices.json
Normal file
File diff suppressed because it is too large
Load Diff
18
elai/filter_json.py
Normal file
18
elai/filter_json.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import json
|
||||
|
||||
# Read JSON from a file
|
||||
input_filename = "english_voices.json"
|
||||
output_filename = "free_english_voices.json"
|
||||
|
||||
with open(input_filename, "r") as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
# Filter entries based on "language": "English"
|
||||
filtered_list = [entry for entry in data["data"]["list"] if not entry["is_paid"]]
|
||||
data["data"]["list"] = filtered_list
|
||||
|
||||
# Write filtered JSON to a new file
|
||||
with open(output_filename, "w") as json_file:
|
||||
json.dump(data, json_file, indent=2)
|
||||
|
||||
print(f"Filtered JSON written to '{output_filename}'.")
|
||||
26579
elai/voices.json
Normal file
26579
elai/voices.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
faiss/ct_focus_tips_index.faiss
Normal file
BIN
faiss/ct_focus_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/language_for_writing_tips_index.faiss
Normal file
BIN
faiss/language_for_writing_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/reading_skill_tips_index.faiss
Normal file
BIN
faiss/reading_skill_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/strategy_tips_index.faiss
Normal file
BIN
faiss/strategy_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/tips_metadata.pkl
Normal file
BIN
faiss/tips_metadata.pkl
Normal file
Binary file not shown.
BIN
faiss/word_link_tips_index.faiss
Normal file
BIN
faiss/word_link_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/word_partners_tips_index.faiss
Normal file
BIN
faiss/word_partners_tips_index.faiss
Normal file
Binary file not shown.
BIN
faiss/writing_skill_tips_index.faiss
Normal file
BIN
faiss/writing_skill_tips_index.faiss
Normal file
Binary file not shown.
13
firebase-configs/encoach-staging.json
Normal file
13
firebase-configs/encoach-staging.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "encoach-staging",
|
||||
"private_key_id": "5718a649419776df9637589f8696a258a6a70f6c",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2C6Es2gY8lLvH\ndVilNtRNm9glSaPXMNw2PzZZbSGuG1uGPFaCzlq1lOb2u17YfMG4GriKIMjIQKXF\nqdvxA8CAmAFRuDjUGmpbO/X1ZW7amOs5Bjed2BYmL01dEqzzwwh7rEfNDjeghRPx\n1uKzH8A6TLT5xq+74I5K1CIgiljBpZimsERu2SDawjkdtZfA7qoylA46Nq66LuwQ\nVyv9CK2SZNpBcT3sunCmRsrCzmSTzKdbcqRPdqUKgZOH/Rjp0sw9VuUgwoxdGZV3\n5SJjObo5ceZ1OSiJm7GwLzp7uq16sqycgSYwppNLI5OtzOfSuWbGD4+a044t2Mlq\n9PHXv7H/AgMBAAECggEAAfhKlFwq8MaL6PggRJq9HbaKgQ4fcOmCmy8AQmPNF1UM\nyVKSKGndjxUfPLCWsaaunUnjZlHoKkvndKXxDyttuVaBE9EiWEqNjRLZ3KpuJ9Jm\nH+CtLbmUCnISQb1n1AlvvZAwhLZbLBL/PhYyWiLapybZAdJAaOWLVKGgBD8gVRQW\nJFCqnszX1O2YlpWHutb979R4qoY/XAf94gyMkTpXZwuETvFqZbau2vxRZ8qARix3\nmic881PwiF6Cod8UPCS9yMK+Q+Se6SomwXU9PCmlummn9xmQBAxYy8gIAVs/J9Fg\n5SvhnImAPDd+zIzzw2cHCiruNWIhroMVZDZJgWdY1QKBgQDjTKKeFOur3ijJJL2/\nWg1SE2jLP0GpXzM5YMx6jdOCNDCzugPngRucRXiTkJ2FnUgyMcQyi6hyrbWXN/6z\nXhx5fwLB4tnTcqOMvNfcay5mDk3RW9ZZJxayB54Sf1Nm/4xiDBnGPT+iHQvK+/pT\nwScWznFkmk60E796o76OLn3PEwKBgQDNCC2uPq+uOcCopIO8HH88gqdxTvpbeHUU\nrdJOmr1VtGNuvay/mfpva9+VEtGbZTFzjhfvfCEIjpj3Llh8Flb9EYa6BmscBiyp\ngszEeFuB3zHndlSCZPnGJ7JiRAdPAEgG3Gl/r9th6PDaEMq0MFS5i7GGhPBIRYCG\nUtmY5eVy5QKBgH5Nuls/YsnJFD7ZNLscziQadvPhvZnhNbSfjmBXaP2EBMAKEFtX\nCcGndN4C0RVLFbAWqWAw7LR0xGA4FEcVd5snsZ+Nb98oZ6sv0H9B67F4J1O7xXsa\n1mitBPBgYjbsr9RXxwa6SB7MJx5vMGXUAeWRZ78wY6V7B76dOKkHOo+TAoGBAJf5\nBOsPueZZFm2qK58GPGVcrsI0+StNuPLP+H+dANQC9mTCIMaQWmm2Oq5jmYwmUKZH\nX4R6rH2MPOOSrbGkWWwRTpyaX1ARX49xzVefoqw8BOB8/Bz+vYjcKcPeitBK9Bhp\nzaUAc4s6PzRTl/xBirtRSQ/df8ECC0cFKBbF6PHlAoGAGqnlpo+k8vAtg6ulCuGu\nx2Y/c5UmvXGHk60pccnW3UtENSDnl99OgMfBz8/qLAMWs6DUQ/kvSlHQPmMBHRWZ\nNTr6ceGXyNs4KdYoj1K7AU3c0Lm0wyQ2giQMoOOUQAm98Xr8z5aiihj10hHPmzzL\n9kwpOmZpjNmC/ERD69imWhY=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-8rs9e@encoach-staging.iam.gserviceaccount.com",
|
||||
"client_id": "108221424237414412378",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-8rs9e%40encoach-staging.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
13
firebase-configs/mti-ielts-626a2dcf6091.json
Normal file
13
firebase-configs/mti-ielts-626a2dcf6091.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "mti-ielts",
|
||||
"private_key_id": "626a2dcf60916a1b5011f388495b8f9c4fc065ef",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDuaLgLNa5yb5LI\nPZYa7qav0URgCF7miK3dUXIBoABQ+U6y1LwdsIiJqHZ4Cm2lotTqeTGOIV83PuA6\n9H/TwnvsHH8jilmsPxO5OX7AyZSDPvN45nJrgQ21RKZCYQGVetBMGhclCRbYFraS\nE6X/p6gSOpSqZ5fLz8BbdCMfib6HSfDmBkYTK42X6d2eNNwLM1wLbE8RmCGwRATC\nQFfMhjlvQcSJ1EDMfkMUUE9U/ux77wfHqs1d+7utVcQTIMFAP9fo1ynJlwp8D1HQ\ntalB6kkpuDQetUR0A1FHMMJekhmuRDUMfokX1F9JfUjR0OetuD3KEH5y2asxC2+0\n8JYcwbvlAgMBAAECggEAKaaW3LJ8rxZp/NyxkDP4YAf9248q0Ti4s00qzzjeRUdA\n5gI/eSphuDb7t34O6NyZOPuCWlPfOB4ee35CpMK59qaF2bYuc2azseznBZRSA1no\nnEsaW0i5Fd2P9FHRPoWtxVXbjEdZu9e//qY7Hn5yYPjmBx1BCkTZ1MBl8HkWlbjR\nbu18uveg5Vg6Wc+rnPmH/gMRLLpq9iQBpzXWT8Mj+k48O8GnW6v8S3R027ymqUou\n3W5b69xDGn0nwxgLIVzdxjoo7RnpjD3mP0x4faiBhScVgFhwZP8hqBeVyqbV5dMh\nfF+p9zLOeilFLJEjH1lZbZAb8wwP23LozIXJWFG3oQKBgQD6COCJ7hNSx9/AzDhO\nh73hKH/KSOJtxHc8795hcZjy9HJkoM45Fm7o2QGZzsZmV+N6VU0BjoDQAyftCq+G\ndIX0wcAGJIsLuQ9K00WI2hn7Uq1gjUl0d9XEorogKa1ZNTLL/9By/xnA7sEpI6Ng\nIsKQ4R2CfqNFU4bs1nyKWCWudQKBgQD0GNYwZt3xV2YBATVYsrvg1OGO/tmkCJ8Y\nLOdM0L+8WMCgw0uQcNFF9uqq6/oFgq7tOvpeZDsY8onRy55saaMT+Lr4xs0sj5B0\ns5Hqc0L37tdXXXXEne8WABMBF9injNgNbAm9W0kqME2Stc53OJQPj2DBdYxWSr8v\n36imCwoJsQKBgH0BBSlQQo7naKFeOGRijvbLpZ//clzIlYh8r+Rtw7brqWlPz+pQ\noeB95cP80coG9K6LiPVXRmU4vrRO3FRPW01ztEod6PpSaifRmnkB+W1h91ZHLMsy\nwkgNxxofXBA2fY/p9FAZ48lGVIH51EtS9Y0zTuqX347gZJtx3E/aI/SlAoGBAJer\nCwM+F2+K352GM7BuNiDoBVLFdVPf64Ko+/sVxdzwxJffYQdZoh634m3bfBmKbsiG\nmeSmoLXKlenefAxewu544SwM0pV6isaIgQTNI3JMXE8ziiZl/5WK7EQEniDVebU1\nSQP4QYjORJUBFE2twQm+C9+I+27uuMa1UOQC/fSxAoGBANuWloacqGfws6nbHvqF\nLZKlkKNPI/0sC+6VlqjoHn5LQz3lcFM1+iKSQIGJvJyru2ODgv2Lmq2W+cx+HMeq\n0BSetK4XtalmO9YflH7uMgvOEVewf4uJ2d+4I1pbY9aI1gHaZ1EUiiy6Ds4kAK8s\nTQqp88pfTbOnkdJBVi0AWs5B\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-dyg6p@mti-ielts.iam.gserviceaccount.com",
|
||||
"client_id": "104980563453519094431",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-dyg6p%40mti-ielts.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
13
firebase-configs/storied-phalanx-349916.json
Normal file
13
firebase-configs/storied-phalanx-349916.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "storied-phalanx-349916",
|
||||
"private_key_id": "c9e05f6fe413b1031a71f981160075ff4b044444",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdgavFB63nMHyb\n38ncwijTrUmqU9UyzNJ8wlZCWAWuoz25Gng988fkKNDXnHY+ap9esHyNYg9IdSA7\nAuZeHpzTZmKiWZzFWq61KWSTgIn1JwKHGHJJdmVhTYfCe9I51cFLa5q2lTFzJ0ce\nbP7/X/7kw53odgva+M8AhDTbe60akpemgZc+LFwO0Abm7erH2HiNyjoNZzNw525L\n933PCaQwhZan04s1u0oRdVlBIBwMk+J0ojgVEpUiJOzF7gkN+UpDXujalLYdlR4q\nhkGgScXQhDYJkECC3GuvOnEo1YXGNjW9D73S6sSH+Lvqta4wW1+sTn0kB6goiQBI\n7cA1G6x3AgMBAAECggEAZPMwAX/adb7XS4LWUNH8IVyccg/63kgSteErxtiu3kRv\nYOj7W+C6fPVNGLap/RBCybjNSvIh3PfkVICh1MtG1eGXmj4VAKyvaskOmVq/hQbe\nVAuEKo7W7V2UPcKIsOsGSQUlYYjlHIIOG4O5Q1HQrRmp4cPK62Txkl6uaEkZPz4u\nbvIK2BJI8aHRwxE3Phw09blwlLqQQQ8nrhK29x5puaN+ft++IlzIOVsLz+n4kTdB\n6qkG/dhenn3K8o3+NkmSN6eNRbdJd36zXTo4Oatbvqb7r0E8vYn/3Llawo2X75zn\nec7jMHrOmcwtiu9H3PsrTWtzdSjxPHy0UtEn1HWK4QKBgQD+c/V8tAvbaUGVoZf6\ntKtDSKF6IHuY2vUO33v950mVdjrTursqOG2d+SLfSnKpc+sjDlj7/S5u4uRP+qUN\ng1rb2U7oIA7tsDa2ZTSkIx6HkPUzS+fBOxELLrbgMoJ2RLzgkiPhS95YgXJ/rYG5\nWQTehzCT5roes0RvtgM0gl3EhQKBgQDe2m7PRIU4g3RJ8HTx92B4ja8W9FVCYDG5\nPOAdZB8WB6Bvu4BJHBDLr8vDi930pKj+vYObRqBDQuILW4t8wZQJ834dnoq6EpUz\nhbVEURVBP4A/nEHrQHfq0Lp+cxThy2rw7obRQOLPETtC7p3WFgSHT6PRTcpGzCCX\n+76a30yrywKBgC/5JNtyBppDaf4QDVtTHMb+tpMT9LmI7pLzR6lDJfhr5gNtPURk\nhyY1hoGaw6t3E2n0lopL3alCVdFObDfz//lbKylQggAGLQqOYjJf/K2KgvA862Df\nBgOZtxjl7PrnUsT0SJd9elotbazsxXxwcB6UVnBMG+MV4V0+b7RCr/MRAoGBAIfp\nTcVIs7roqOZjKN9dEE/VkR/9uXW2tvyS/NfP9Ql5c0ZRYwazgCbJOwsyZRZLyek6\naWYsp5b91mA435QhdwiuoI6t30tmA+qdNBTLIpxdfvjMcoNoGPpzfBmcU/L1HW58\n+mnqGalRiAPlBQvI99ASKQWAXMnaulIWrYNEhj0LAoGBALi+QZ2pp+hDeC59ezWr\nbP1zbbONceHKGgJcevChP2k1OJyIOIqmBYeTuM4cPc5ofZYQNaMC31cs8SVeSRX1\nNTxQZmvCjMyTe/WYWYNFXdgkVz4egFXbeochCGzMYo57HV1PCkPBrARRZO8OfdDD\n8sDu//ohb7nCzceEI0DnWs13\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-3ml0u@storied-phalanx-349916.iam.gserviceaccount.com",
|
||||
"client_id": "114163760341944984396",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-3ml0u%40storied-phalanx-349916.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import jwt
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Define the payload (data to be included in the token)
|
||||
payload = {'sub': 'test'}
|
||||
|
||||
# Define the secret key
|
||||
secret_key = os.getenv("JWT_SECRET_KEY")
|
||||
|
||||
# Generate the JWT
|
||||
jwt_token = jwt.encode(payload, secret_key, algorithm='HS256')
|
||||
|
||||
print(jwt_token)
|
||||
@@ -1,5 +0,0 @@
|
||||
import secrets
|
||||
|
||||
jwt_secret_key = secrets.token_hex(32)
|
||||
|
||||
print(jwt_secret_key)
|
||||
@@ -1,53 +0,0 @@
|
||||
import json
|
||||
import openai
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
|
||||
MAX_TOKENS = 4097
|
||||
TOP_P = 0.9
|
||||
FREQUENCY_PENALTY = 0.5
|
||||
|
||||
TRY_LIMIT = 1
|
||||
|
||||
try_count = 0
|
||||
def process_response(input_string):
|
||||
json_obj = {}
|
||||
parsed_string = input_string.replace("'", "\"")
|
||||
parsed_string = parsed_string.replace("\n\n", " ")
|
||||
try:
|
||||
json_obj = json.loads(parsed_string)
|
||||
except json.JSONDecodeError:
|
||||
print("Invalid JSON string!")
|
||||
|
||||
return json_obj
|
||||
|
||||
def check_fields(obj, fields):
|
||||
return all(field in obj for field in fields)
|
||||
|
||||
def make_openai_call(messages, token_count, fields_to_check, temperature):
|
||||
global try_count
|
||||
result = openai.ChatCompletion.create(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=int(MAX_TOKENS - token_count - 300),
|
||||
temperature=float(temperature),
|
||||
top_p=float(TOP_P),
|
||||
frequency_penalty=float(FREQUENCY_PENALTY),
|
||||
messages=messages
|
||||
)
|
||||
processed_response = process_response(result["choices"][0]["message"]["content"])
|
||||
if check_fields(processed_response, fields_to_check) is False and try_count < TRY_LIMIT:
|
||||
try_count = try_count + 1
|
||||
return make_openai_call(messages, token_count, fields_to_check)
|
||||
elif try_count >= TRY_LIMIT:
|
||||
try_count = 0
|
||||
return result["choices"][0]["message"]["content"]
|
||||
else:
|
||||
try_count = 0
|
||||
return processed_response
|
||||
|
||||
|
||||
156
ielts_be/__init__.py
Normal file
156
ielts_be/__init__.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
|
||||
import aioboto3
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import nltk
|
||||
from starlette import status
|
||||
|
||||
from ielts_be.api import router
|
||||
from ielts_be.configs import DependencyInjector
|
||||
from ielts_be.exceptions import CustomException
|
||||
from ielts_be.middlewares import AuthenticationMiddleware, AuthBackend
|
||||
from ielts_be.services.impl import OpenAIWhisper
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
"""
|
||||
Startup and Shutdown logic is in this lifespan method
|
||||
|
||||
https://fastapi.tiangolo.com/advanced/events/
|
||||
"""
|
||||
|
||||
# NLTK required datasets download
|
||||
nltk.download('words')
|
||||
nltk.download("punkt")
|
||||
|
||||
# AWS Polly client instantiation
|
||||
context_stack = contextlib.AsyncExitStack()
|
||||
session = aioboto3.Session()
|
||||
polly_client = await context_stack.enter_async_context(
|
||||
session.client(
|
||||
'polly',
|
||||
region_name='eu-west-1',
|
||||
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID")
|
||||
)
|
||||
)
|
||||
|
||||
http_client = httpx.AsyncClient()
|
||||
stt = OpenAIWhisper()
|
||||
|
||||
DependencyInjector(
|
||||
polly_client,
|
||||
http_client,
|
||||
stt
|
||||
).inject()
|
||||
|
||||
# Setup logging
|
||||
config_file = pathlib.Path("./ielts_be/configs/logging/logging_config.json")
|
||||
with open(config_file) as f_in:
|
||||
config = json.load(f_in)
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
yield
|
||||
|
||||
stt.close()
|
||||
await http_client.aclose()
|
||||
await polly_client.close()
|
||||
await context_stack.aclose()
|
||||
|
||||
|
||||
def setup_listeners(_app: FastAPI) -> None:
|
||||
@_app.exception_handler(RequestValidationError)
|
||||
async def custom_form_validation_error(request, exc):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
reformatted_message = defaultdict(list)
|
||||
for pydantic_error in exc.errors():
|
||||
loc, msg = pydantic_error["loc"], pydantic_error["msg"]
|
||||
filtered_loc = loc[1:] if loc[0] in ("body", "query", "path") else loc
|
||||
field_string = ".".join(filtered_loc)
|
||||
if field_string == "cookie.refresh_token":
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"error_code": 401, "message": HTTPStatus.UNAUTHORIZED.description},
|
||||
)
|
||||
reformatted_message[field_string].append(msg)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content=jsonable_encoder(
|
||||
{"details": "Invalid request!", "errors": reformatted_message}
|
||||
),
|
||||
)
|
||||
|
||||
@_app.exception_handler(CustomException)
|
||||
async def custom_exception_handler(request: Request, exc: CustomException):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content={"error_code": exc.error_code, "message": exc.message},
|
||||
)
|
||||
|
||||
@_app.exception_handler(Exception)
|
||||
async def default_exception_handler(request: Request, exc: Exception):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def setup_middleware() -> List[Middleware]:
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
),
|
||||
Middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=AuthBackend()
|
||||
)
|
||||
]
|
||||
return middleware
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
env = os.getenv("ENV")
|
||||
_app = FastAPI(
|
||||
docs_url="/docs" if env != "production" else None,
|
||||
redoc_url="/redoc" if env != "production" else None,
|
||||
middleware=setup_middleware(),
|
||||
lifespan=lifespan
|
||||
)
|
||||
_app.include_router(router)
|
||||
setup_listeners(_app)
|
||||
return _app
|
||||
|
||||
|
||||
app = create_app()
|
||||
15
ielts_be/api/__init__.py
Normal file
15
ielts_be/api/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .training import training_router
|
||||
from .user import user_router
|
||||
from .exam import exam_router
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Home"])
|
||||
|
||||
@router.get('/healthcheck')
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
|
||||
router.include_router(training_router, prefix="/training", tags=["Training"])
|
||||
router.include_router(user_router, prefix="/user", tags=["Users"])
|
||||
router.include_router(exam_router)
|
||||
16
ielts_be/api/exam/__init__.py
Normal file
16
ielts_be/api/exam/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .listening import listening_router
|
||||
from .reading import reading_router
|
||||
from .speaking import speaking_router
|
||||
from .writing import writing_router
|
||||
from .level import level_router
|
||||
from .grade import grade_router
|
||||
|
||||
exam_router = APIRouter()
|
||||
exam_router.include_router(listening_router, prefix="/listening", tags=["Listening"])
|
||||
exam_router.include_router(reading_router, prefix="/reading", tags=["Reading"])
|
||||
exam_router.include_router(speaking_router, prefix="/speaking", tags=["Speaking"])
|
||||
exam_router.include_router(writing_router, prefix="/writing", tags=["Writing"])
|
||||
exam_router.include_router(level_router, prefix="/level", tags=["Level"])
|
||||
exam_router.include_router(grade_router, prefix="/grade", tags=["Grade"])
|
||||
64
ielts_be/api/exam/grade.py
Normal file
64
ielts_be/api/exam/grade.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Depends, Path, Request, BackgroundTasks
|
||||
|
||||
from ielts_be.controllers import IGradeController
|
||||
from ielts_be.dtos.writing import WritingGradeTaskDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
controller = "grade_controller"
|
||||
grade_router = APIRouter()
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/writing/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_writing_task(
|
||||
data: WritingGradeTaskDTO,
|
||||
background_tasks: BackgroundTasks,
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_writing_task(task, data, background_tasks)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/speaking/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_speaking_task(
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
task: int = Path(..., ge=1, le=3),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
form = await request.form()
|
||||
return await grade_controller.grade_speaking_task(task, form, background_tasks)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/summary',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grading_summary(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grading_summary(data)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/short_answers',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_short_answers(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grade_short_answers(data)
|
||||
67
ielts_be/api/exam/level.py
Normal file
67
ielts_be/api/exam/level.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, UploadFile, Request
|
||||
|
||||
from ielts_be.dtos.level import LevelExercisesDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import ILevelController
|
||||
|
||||
controller = "level_controller"
|
||||
level_router = APIRouter()
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_exercises(
|
||||
dto: LevelExercisesDTO,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.generate_exercises(dto)
|
||||
|
||||
@level_router.get(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_exam(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_exam()
|
||||
|
||||
|
||||
@level_router.get(
|
||||
'/utas',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_utas(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_utas()
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/import/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def import_level(
|
||||
exercises: UploadFile,
|
||||
solutions: UploadFile = None,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.upload_level(exercises, solutions)
|
||||
|
||||
@level_router.post(
|
||||
'/custom/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def custom_level(
|
||||
request: Request,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await level_controller.get_custom_level(data)
|
||||
89
ielts_be/api/exam/listening.py
Normal file
89
ielts_be/api/exam/listening.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile
|
||||
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IListeningController
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.dtos.listening import ListeningExercisesDTO, Dialog, InstructionsDTO
|
||||
|
||||
controller = "listening_controller"
|
||||
listening_router = APIRouter()
|
||||
|
||||
@listening_router.post(
|
||||
'/import',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def upload(
|
||||
exercises: UploadFile,
|
||||
solutions: UploadFile = None,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.import_exam(exercises, solutions)
|
||||
|
||||
|
||||
@listening_router.get(
|
||||
'/{section}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_listening_dialog(
|
||||
section: int = Path(..., ge=1, le=4),
|
||||
difficulty: str = Query(default=None),
|
||||
topic: str = Query(default=None),
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
topic = random.choice(EducationalContent.TOPICS) if not topic else topic
|
||||
return await listening_controller.generate_listening_dialog(section, topic, difficulty)
|
||||
|
||||
@listening_router.post(
|
||||
'/media',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_mp3(
|
||||
dto: Dialog,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.generate_mp3(dto)
|
||||
|
||||
|
||||
|
||||
@listening_router.post(
|
||||
'/transcribe',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def transcribe_dialog(
|
||||
audio: UploadFile,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.transcribe_dialog(audio)
|
||||
|
||||
|
||||
@listening_router.post(
|
||||
'/instructions',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def create_instructions(
|
||||
dto: InstructionsDTO,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.create_instructions(dto.text)
|
||||
|
||||
|
||||
@listening_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_listening_exercise(
|
||||
dto: ListeningExercisesDTO,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.get_listening_question(dto)
|
||||
|
||||
51
ielts_be/api/exam/reading.py
Normal file
51
ielts_be/api/exam/reading.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile
|
||||
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.dtos.reading import ReadingDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IReadingController
|
||||
|
||||
controller = "reading_controller"
|
||||
reading_router = APIRouter()
|
||||
|
||||
|
||||
@reading_router.post(
|
||||
'/import',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def upload(
|
||||
exercises: UploadFile,
|
||||
solutions: UploadFile = None,
|
||||
reading_controller: IReadingController = Depends(Provide[controller])
|
||||
):
|
||||
return await reading_controller.import_exam(exercises, solutions)
|
||||
|
||||
@reading_router.get(
|
||||
'/{passage}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_passage(
|
||||
topic: Optional[str] = Query(None),
|
||||
word_count: Optional[int] = Query(None),
|
||||
passage: int = Path(..., ge=1, le=3),
|
||||
reading_controller: IReadingController = Depends(Provide[controller])
|
||||
):
|
||||
topic = random.choice(EducationalContent.TOPICS) if not topic else topic
|
||||
return await reading_controller.generate_reading_passage(passage, topic, word_count)
|
||||
|
||||
@reading_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_reading(
|
||||
dto: ReadingDTO,
|
||||
reading_controller: IReadingController = Depends(Provide[controller])
|
||||
):
|
||||
return await reading_controller.generate_reading_exercises(dto)
|
||||
74
ielts_be/api/exam/speaking.py
Normal file
74
ielts_be/api/exam/speaking.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import random
|
||||
from typing import Optional, List
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends
|
||||
|
||||
from ielts_be.dtos.speaking import Video
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.controllers import ISpeakingController
|
||||
|
||||
controller = "speaking_controller"
|
||||
speaking_router = APIRouter()
|
||||
|
||||
@speaking_router.post(
|
||||
'/media',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video(
|
||||
video: Video,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(video.text, video.avatar)
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/media/{vid_id}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def poll_video(
|
||||
vid_id: str = Path(...),
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.poll_video(vid_id)
|
||||
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/avatars',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_avatars(
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.get_avatars()
|
||||
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_speaking_task(
|
||||
task: int = Path(..., ge=1, le=3),
|
||||
topic: Optional[str] = Query(None),
|
||||
first_topic: Optional[str] = Query(None),
|
||||
second_topic: Optional[str] = Query(None),
|
||||
difficulty: Optional[str] = None,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
if not second_topic:
|
||||
topic_or_first_topic = topic if topic else random.choice(EducationalContent.MTI_TOPICS)
|
||||
else:
|
||||
topic_or_first_topic = first_topic if first_topic else random.choice(EducationalContent.MTI_TOPICS)
|
||||
|
||||
if not difficulty:
|
||||
difficulty = random.choice(random.choice(EducationalContent.DIFFICULTIES))
|
||||
|
||||
second_topic = second_topic if second_topic else random.choice(EducationalContent.MTI_TOPICS)
|
||||
return await speaking_controller.get_speaking_part(task, topic_or_first_topic, second_topic, difficulty)
|
||||
43
ielts_be/api/exam/writing.py
Normal file
43
ielts_be/api/exam/writing.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import random
|
||||
from typing import Optional, List
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends, UploadFile, File
|
||||
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.controllers import IWritingController
|
||||
|
||||
controller = "writing_controller"
|
||||
writing_router = APIRouter()
|
||||
|
||||
|
||||
@writing_router.post(
|
||||
'/{task}/attachment',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_writing_academic(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
file: UploadFile = File(...),
|
||||
difficulty: Optional[List[str]] = None,
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
return await writing_controller.get_writing_task_academic_question(task, file, difficulty)
|
||||
|
||||
|
||||
@writing_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_writing(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
difficulty: Optional[str] = None,
|
||||
topic: str = Query(default=None),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic
|
||||
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)
|
||||
9
ielts_be/api/home.py
Normal file
9
ielts_be/api/home.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
home_router = APIRouter()
|
||||
|
||||
|
||||
@home_router.get(
|
||||
'/healthcheck'
|
||||
)
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
34
ielts_be/api/training.py
Normal file
34
ielts_be/api/training.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from ielts_be.dtos.training import FetchTipsDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import ITrainingController
|
||||
|
||||
controller = "training_controller"
|
||||
training_router = APIRouter()
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/tips',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_reading_passage(
|
||||
data: FetchTipsDTO,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
return await training_controller.fetch_tips(data)
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def training_content(
|
||||
request: Request,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await training_controller.get_training_content(data)
|
||||
21
ielts_be/api/user.py
Normal file
21
ielts_be/api/user.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ielts_be.dtos.user_batch import BatchUsersDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IUserController
|
||||
|
||||
controller = "user_controller"
|
||||
user_router = APIRouter()
|
||||
|
||||
|
||||
@user_router.post(
|
||||
'/import',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def batch_import(
|
||||
batch: BatchUsersDTO,
|
||||
user_controller: IUserController = Depends(Provide[controller])
|
||||
):
|
||||
return await user_controller.batch_import(batch)
|
||||
5
ielts_be/configs/__init__.py
Normal file
5
ielts_be/configs/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .dependency_injection import DependencyInjector
|
||||
|
||||
__all__ = [
|
||||
"DependencyInjector"
|
||||
]
|
||||
780
ielts_be/configs/constants.py
Normal file
780
ielts_be/configs/constants.py
Normal file
@@ -0,0 +1,780 @@
|
||||
from enum import Enum
|
||||
|
||||
########################################################################################################################
|
||||
# DISCLAIMER #
|
||||
# #
|
||||
# All the array and dict "constants" are mutable variables, if somewhere in the app you modify them in any way, shape #
|
||||
# or form all the other methods that will use these "constants" will also use the modified version. If you're unsure #
|
||||
# whether a method will modify it use copy's deepcopy: #
|
||||
# #
|
||||
# from copy import deepcopy #
|
||||
# #
|
||||
# new_ref = deepcopy(CONSTANT) #
|
||||
# #
|
||||
# Using a wrapper method that returns a "constant" won't handle nested mutables. #
|
||||
########################################################################################################################
|
||||
|
||||
BLACKLISTED_WORDS = ["jesus", "sex", "gay", "lesbian", "homosexual", "god", "angel", "pornography", "beer", "wine",
|
||||
"cocaine", "alcohol", "nudity", "lgbt", "casino", "gambling", "catholicism",
|
||||
"discrimination", "politic", "christianity", "islam", "christian", "christians",
|
||||
"jews", "jew", "discrimination", "discriminatory"]
|
||||
|
||||
|
||||
class UserDefaults:
|
||||
DESIRED_LEVELS = {
|
||||
"reading": 9,
|
||||
"listening": 9,
|
||||
"writing": 9,
|
||||
"speaking": 9,
|
||||
}
|
||||
|
||||
LEVELS = {
|
||||
"reading": 0,
|
||||
"listening": 0,
|
||||
"writing": 0,
|
||||
"speaking": 0,
|
||||
}
|
||||
|
||||
|
||||
class ExamVariant(Enum):
|
||||
FULL = "full"
|
||||
PARTIAL = "partial"
|
||||
|
||||
|
||||
class ReadingExerciseType(str, Enum):
|
||||
fillBlanks = "fillBlanks"
|
||||
writeBlanks = "writeBlanks"
|
||||
trueFalse = "trueFalse"
|
||||
paragraphMatch = "paragraphMatch"
|
||||
ideaMatch = "ideaMatch"
|
||||
multipleChoice = "multipleChoice"
|
||||
|
||||
|
||||
class ListeningExerciseType(str, Enum):
|
||||
multipleChoice = "multipleChoice"
|
||||
multipleChoice3Options = "multipleChoice3Options"
|
||||
writeBlanksQuestions = "writeBlanksQuestions"
|
||||
writeBlanksFill = "writeBlanksFill"
|
||||
writeBlanksForm = "writeBlanksForm"
|
||||
trueFalse = "trueFalse"
|
||||
|
||||
class LevelExerciseType(str, Enum):
|
||||
multipleChoice = "multipleChoice"
|
||||
mcBlank = "mcBlank"
|
||||
mcUnderline = "mcUnderline"
|
||||
blankSpace = "blankSpaceText"
|
||||
passageUtas = "passageUtas"
|
||||
fillBlanksMC = "fillBlanksMC"
|
||||
|
||||
|
||||
class CustomLevelExerciseTypes(Enum):
|
||||
MULTIPLE_CHOICE_4 = "multiple_choice_4"
|
||||
MULTIPLE_CHOICE_BLANK_SPACE = "multiple_choice_blank_space"
|
||||
MULTIPLE_CHOICE_UNDERLINED = "multiple_choice_underlined"
|
||||
BLANK_SPACE_TEXT = "blank_space_text"
|
||||
READING_PASSAGE_UTAS = "reading_passage_utas"
|
||||
WRITING_LETTER = "writing_letter"
|
||||
WRITING_2 = "writing_2"
|
||||
SPEAKING_1 = "speaking_1"
|
||||
SPEAKING_2 = "speaking_2"
|
||||
SPEAKING_3 = "speaking_3"
|
||||
READING_1 = "reading_1"
|
||||
READING_2 = "reading_2"
|
||||
READING_3 = "reading_3"
|
||||
LISTENING_1 = "listening_1"
|
||||
LISTENING_2 = "listening_2"
|
||||
LISTENING_3 = "listening_3"
|
||||
LISTENING_4 = "listening_4"
|
||||
|
||||
|
||||
class QuestionType(Enum):
|
||||
LISTENING_SECTION_1 = "Listening Section 1"
|
||||
LISTENING_SECTION_2 = "Listening Section 2"
|
||||
LISTENING_SECTION_3 = "Listening Section 3"
|
||||
LISTENING_SECTION_4 = "Listening Section 4"
|
||||
WRITING_TASK_1 = "Writing Task 1"
|
||||
WRITING_TASK_2 = "Writing Task 2"
|
||||
SPEAKING_1 = "Speaking Task Part 1"
|
||||
SPEAKING_2 = "Speaking Task Part 2"
|
||||
READING_PASSAGE_1 = "Reading Passage 1"
|
||||
READING_PASSAGE_2 = "Reading Passage 2"
|
||||
READING_PASSAGE_3 = "Reading Passage 3"
|
||||
|
||||
|
||||
class FilePaths:
|
||||
AUDIO_FILES_PATH = 'download-audio/'
|
||||
FIREBASE_LISTENING_AUDIO_FILES_PATH = 'listening_recordings/'
|
||||
VIDEO_FILES_PATH = 'download-video/'
|
||||
FIREBASE_SPEAKING_VIDEO_FILES_PATH = 'speaking_videos/'
|
||||
FIREBASE_FAILED_TRANSCRIPTION_FILES_PATH = 'failed_transcriptions/'
|
||||
WRITING_ATTACHMENTS = 'writing_attachments/'
|
||||
|
||||
|
||||
class TemperatureSettings:
|
||||
GRADING_TEMPERATURE = 0.1
|
||||
TIPS_TEMPERATURE = 0.2
|
||||
GEN_QUESTION_TEMPERATURE = 0.7
|
||||
|
||||
|
||||
class GPTModels:
|
||||
GPT_3_5_TURBO = "gpt-3.5-turbo"
|
||||
GPT_4_TURBO = "gpt-4-turbo"
|
||||
GPT_4_O = "gpt-4o"
|
||||
GPT_3_5_TURBO_16K = "gpt-3.5-turbo-16k"
|
||||
GPT_3_5_TURBO_INSTRUCT = "gpt-3.5-turbo-instruct"
|
||||
GPT_4_PREVIEW = "gpt-4-turbo-preview"
|
||||
|
||||
|
||||
class FieldsAndExercises:
|
||||
GRADING_FIELDS = ['comment', 'overall', 'task_response']
|
||||
GEN_FIELDS = ['topic']
|
||||
GEN_TEXT_FIELDS = ['title']
|
||||
LISTENING_GEN_FIELDS = ['transcript', 'exercise']
|
||||
READING_EXERCISE_TYPES = ['fillBlanks', 'writeBlanks', 'trueFalse', 'paragraphMatch']
|
||||
READING_3_EXERCISE_TYPES = ['fillBlanks', 'writeBlanks', 'trueFalse', 'paragraphMatch', 'ideaMatch']
|
||||
|
||||
LISTENING_EXERCISE_TYPES = ['multipleChoice', 'writeBlanksQuestions', 'writeBlanksFill', 'writeBlanksForm']
|
||||
LISTENING_1_EXERCISE_TYPES = ['multipleChoice', 'writeBlanksQuestions', 'writeBlanksFill', 'writeBlanksFill',
|
||||
'writeBlanksForm', 'writeBlanksForm', 'writeBlanksForm', 'writeBlanksForm']
|
||||
LISTENING_2_EXERCISE_TYPES = ['multipleChoice', 'writeBlanksQuestions']
|
||||
LISTENING_3_EXERCISE_TYPES = ['multipleChoice3Options', 'writeBlanksQuestions']
|
||||
LISTENING_4_EXERCISE_TYPES = ['multipleChoice', 'writeBlanksQuestions', 'writeBlanksFill', 'writeBlanksForm']
|
||||
|
||||
TOTAL_READING_PASSAGE_1_EXERCISES = 13
|
||||
TOTAL_READING_PASSAGE_2_EXERCISES = 13
|
||||
TOTAL_READING_PASSAGE_3_EXERCISES = 14
|
||||
|
||||
TOTAL_LISTENING_SECTION_1_EXERCISES = 10
|
||||
TOTAL_LISTENING_SECTION_2_EXERCISES = 10
|
||||
TOTAL_LISTENING_SECTION_3_EXERCISES = 10
|
||||
TOTAL_LISTENING_SECTION_4_EXERCISES = 10
|
||||
|
||||
|
||||
class MinTimers:
|
||||
LISTENING_MIN_TIMER_DEFAULT = 30
|
||||
WRITING_MIN_TIMER_DEFAULT = 60
|
||||
SPEAKING_MIN_TIMER_DEFAULT = 14
|
||||
|
||||
|
||||
class Voices:
|
||||
EN_US_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Salli', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Salli',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Male', 'Id': 'Matthew', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Matthew',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Female', 'Id': 'Kimberly', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Kimberly',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Female', 'Id': 'Kendra', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Kendra',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Male', 'Id': 'Justin', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Justin',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Male', 'Id': 'Joey', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Joey',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Female', 'Id': 'Joanna', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Joanna',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Female', 'Id': 'Ivy', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Ivy',
|
||||
'SupportedEngines': ['neural', 'standard']}]
|
||||
EN_GB_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Emma', 'LanguageCode': 'en-GB', 'LanguageName': 'British English', 'Name': 'Emma',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Male', 'Id': 'Brian', 'LanguageCode': 'en-GB', 'LanguageName': 'British English', 'Name': 'Brian',
|
||||
'SupportedEngines': ['neural', 'standard']},
|
||||
{'Gender': 'Female', 'Id': 'Amy', 'LanguageCode': 'en-GB', 'LanguageName': 'British English', 'Name': 'Amy',
|
||||
'SupportedEngines': ['neural', 'standard']}]
|
||||
EN_GB_WLS_VOICES = [
|
||||
{'Gender': 'Male', 'Id': 'Geraint', 'LanguageCode': 'en-GB-WLS', 'LanguageName': 'Welsh English', 'Name': 'Geraint',
|
||||
'SupportedEngines': ['standard']}]
|
||||
EN_AU_VOICES = [{'Gender': 'Male', 'Id': 'Russell', 'LanguageCode': 'en-AU', 'LanguageName': 'Australian English',
|
||||
'Name': 'Russell', 'SupportedEngines': ['standard']},
|
||||
{'Gender': 'Female', 'Id': 'Nicole', 'LanguageCode': 'en-AU', 'LanguageName': 'Australian English',
|
||||
'Name': 'Nicole', 'SupportedEngines': ['standard']}]
|
||||
|
||||
ALL_VOICES = EN_US_VOICES + EN_GB_VOICES + EN_GB_WLS_VOICES + EN_AU_VOICES
|
||||
|
||||
MALE_VOICES = [item for item in ALL_VOICES if item.get('Gender') == 'Male']
|
||||
FEMALE_VOICES = [item for item in ALL_VOICES if item.get('Gender') == 'Female']
|
||||
|
||||
|
||||
class NeuralVoices:
|
||||
NEURAL_EN_US_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Danielle', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Danielle',
|
||||
'SupportedEngines': ['neural']},
|
||||
{'Gender': 'Male', 'Id': 'Gregory', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Gregory',
|
||||
'SupportedEngines': ['neural']},
|
||||
{'Gender': 'Male', 'Id': 'Kevin', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Kevin',
|
||||
'SupportedEngines': ['neural']},
|
||||
{'Gender': 'Female', 'Id': 'Ruth', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Ruth',
|
||||
'SupportedEngines': ['neural']},
|
||||
{'Gender': 'Male', 'Id': 'Stephen', 'LanguageCode': 'en-US', 'LanguageName': 'US English', 'Name': 'Stephen',
|
||||
'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_GB_VOICES = [
|
||||
{'Gender': 'Male', 'Id': 'Arthur', 'LanguageCode': 'en-GB', 'LanguageName': 'British English', 'Name': 'Arthur',
|
||||
'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_AU_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Olivia', 'LanguageCode': 'en-AU', 'LanguageName': 'Australian English',
|
||||
'Name': 'Olivia', 'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_ZA_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Ayanda', 'LanguageCode': 'en-ZA', 'LanguageName': 'South African English',
|
||||
'Name': 'Ayanda', 'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_NZ_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Aria', 'LanguageCode': 'en-NZ', 'LanguageName': 'New Zealand English', 'Name': 'Aria',
|
||||
'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_IN_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Kajal', 'LanguageCode': 'en-IN', 'LanguageName': 'Indian English', 'Name': 'Kajal',
|
||||
'SupportedEngines': ['neural']}]
|
||||
NEURAL_EN_IE_VOICES = [
|
||||
{'Gender': 'Female', 'Id': 'Niamh', 'LanguageCode': 'en-IE', 'LanguageName': 'Irish English', 'Name': 'Niamh',
|
||||
'SupportedEngines': ['neural']}]
|
||||
|
||||
ALL_NEURAL_VOICES = NEURAL_EN_US_VOICES + NEURAL_EN_GB_VOICES + NEURAL_EN_AU_VOICES + NEURAL_EN_ZA_VOICES + NEURAL_EN_NZ_VOICES + NEURAL_EN_IE_VOICES
|
||||
|
||||
MALE_NEURAL_VOICES = [item for item in ALL_NEURAL_VOICES if item.get('Gender') == 'Male']
|
||||
FEMALE_NEURAL_VOICES = [item for item in ALL_NEURAL_VOICES if item.get('Gender') == 'Female']
|
||||
|
||||
|
||||
class EducationalContent:
|
||||
DIFFICULTIES = ["A1", "A2", "B1", "B2", "C1", "C2"]
|
||||
|
||||
MTI_TOPICS = [
|
||||
"Education",
|
||||
"Technology",
|
||||
"Environment",
|
||||
"Health and Fitness",
|
||||
"Engineering",
|
||||
"Work and Careers",
|
||||
"Travel and Tourism",
|
||||
"Culture and Traditions",
|
||||
"Social Issues",
|
||||
"Arts and Entertainment",
|
||||
"Climate Change",
|
||||
"Social Media",
|
||||
"Sustainable Development",
|
||||
"Health Care",
|
||||
"Immigration",
|
||||
"Artificial Intelligence",
|
||||
"Consumerism",
|
||||
"Online Shopping",
|
||||
"Energy",
|
||||
"Oil and Gas",
|
||||
"Poverty and Inequality",
|
||||
"Cultural Diversity",
|
||||
"Democracy and Governance",
|
||||
"Mental Health",
|
||||
"Ethics and Morality",
|
||||
"Population Growth",
|
||||
"Science and Innovation",
|
||||
"Poverty Alleviation",
|
||||
"Cybersecurity and Privacy",
|
||||
"Human Rights",
|
||||
"Food and Agriculture",
|
||||
"Cyberbullying and Online Safety",
|
||||
"Linguistic Diversity",
|
||||
"Urbanization",
|
||||
"Artificial Intelligence in Education",
|
||||
"Youth Empowerment",
|
||||
"Disaster Management",
|
||||
"Mental Health Stigma",
|
||||
"Internet Censorship",
|
||||
"Sustainable Fashion",
|
||||
"Indigenous Rights",
|
||||
"Water Scarcity",
|
||||
"Social Entrepreneurship",
|
||||
"Privacy in the Digital Age",
|
||||
"Sustainable Transportation",
|
||||
"Gender Equality",
|
||||
"Automation and Job Displacement",
|
||||
"Digital Divide",
|
||||
"Education Inequality"
|
||||
]
|
||||
TOPICS = [
|
||||
"Art and Creativity",
|
||||
"History of Ancient Civilizations",
|
||||
"Environmental Conservation",
|
||||
"Space Exploration",
|
||||
"Artificial Intelligence",
|
||||
"Climate Change",
|
||||
"The Human Brain",
|
||||
"Renewable Energy",
|
||||
"Cultural Diversity",
|
||||
"Modern Technology Trends",
|
||||
"Sustainable Agriculture",
|
||||
"Natural Disasters",
|
||||
"Cybersecurity",
|
||||
"Philosophy of Ethics",
|
||||
"Robotics",
|
||||
"Health and Wellness",
|
||||
"Literature and Classics",
|
||||
"World Geography",
|
||||
"Social Media Impact",
|
||||
"Food Sustainability",
|
||||
"Economics and Markets",
|
||||
"Human Evolution",
|
||||
"Political Systems",
|
||||
"Mental Health Awareness",
|
||||
"Quantum Physics",
|
||||
"Biodiversity",
|
||||
"Education Reform",
|
||||
"Animal Rights",
|
||||
"The Industrial Revolution",
|
||||
"Future of Work",
|
||||
"Film and Cinema",
|
||||
"Genetic Engineering",
|
||||
"Climate Policy",
|
||||
"Space Travel",
|
||||
"Renewable Energy Sources",
|
||||
"Cultural Heritage Preservation",
|
||||
"Modern Art Movements",
|
||||
"Sustainable Transportation",
|
||||
"The History of Medicine",
|
||||
"Artificial Neural Networks",
|
||||
"Climate Adaptation",
|
||||
"Philosophy of Existence",
|
||||
"Augmented Reality",
|
||||
"Yoga and Meditation",
|
||||
"Literary Genres",
|
||||
"World Oceans",
|
||||
"Social Networking",
|
||||
"Sustainable Fashion",
|
||||
"Prehistoric Era",
|
||||
"Democracy and Governance",
|
||||
"Postcolonial Literature",
|
||||
"Geopolitics",
|
||||
"Psychology and Behavior",
|
||||
"Nanotechnology",
|
||||
"Endangered Species",
|
||||
"Education Technology",
|
||||
"Renaissance Art",
|
||||
"Renewable Energy Policy",
|
||||
"Modern Architecture",
|
||||
"Climate Resilience",
|
||||
"Artificial Life",
|
||||
"Fitness and Nutrition",
|
||||
"Classic Literature Adaptations",
|
||||
"Ethical Dilemmas",
|
||||
"Internet of Things (IoT)",
|
||||
"Meditation Practices",
|
||||
"Literary Symbolism",
|
||||
"Marine Conservation",
|
||||
"Sustainable Tourism",
|
||||
"Ancient Philosophy",
|
||||
"Cold War Era",
|
||||
"Behavioral Economics",
|
||||
"Space Colonization",
|
||||
"Clean Energy Initiatives",
|
||||
"Cultural Exchange",
|
||||
"Modern Sculpture",
|
||||
"Climate Mitigation",
|
||||
"Mindfulness",
|
||||
"Literary Criticism",
|
||||
"Wildlife Conservation",
|
||||
"Renewable Energy Innovations",
|
||||
"History of Mathematics",
|
||||
"Human-Computer Interaction",
|
||||
"Global Health",
|
||||
"Cultural Appropriation",
|
||||
"Traditional cuisine and culinary arts",
|
||||
"Local music and dance traditions",
|
||||
"History of the region and historical landmarks",
|
||||
"Traditional crafts and artisanal skills",
|
||||
"Wildlife and conservation efforts",
|
||||
"Local sports and athletic competitions",
|
||||
"Fashion trends and clothing styles",
|
||||
"Education systems and advancements",
|
||||
"Healthcare services and medical innovations",
|
||||
"Family values and social dynamics",
|
||||
"Travel destinations and tourist attractions",
|
||||
"Environmental sustainability projects",
|
||||
"Technological developments and innovations",
|
||||
"Entrepreneurship and business ventures",
|
||||
"Youth empowerment initiatives",
|
||||
"Art exhibitions and cultural events",
|
||||
"Philanthropy and community development projects"
|
||||
]
|
||||
|
||||
TWO_PEOPLE_SCENARIOS = [
|
||||
"Booking a table at a restaurant",
|
||||
"Making a doctor's appointment",
|
||||
"Asking for directions to a tourist attraction",
|
||||
"Inquiring about public transportation options",
|
||||
"Discussing weekend plans with a friend",
|
||||
"Ordering food at a café",
|
||||
"Renting a bicycle for a day",
|
||||
"Arranging a meeting with a colleague",
|
||||
"Talking to a real estate agent about renting an apartment",
|
||||
"Discussing travel plans for an upcoming vacation",
|
||||
"Checking the availability of a hotel room",
|
||||
"Talking to a car rental service",
|
||||
"Asking for recommendations at a library",
|
||||
"Inquiring about opening hours at a museum",
|
||||
"Discussing the weather forecast",
|
||||
"Shopping for groceries",
|
||||
"Renting a movie from a video store",
|
||||
"Booking a flight ticket",
|
||||
"Discussing a school assignment with a classmate",
|
||||
"Making a reservation for a spa appointment",
|
||||
"Talking to a customer service representative about a product issue",
|
||||
"Discussing household chores with a family member",
|
||||
"Planning a surprise party for a friend",
|
||||
"Talking to a coworker about a project deadline",
|
||||
"Inquiring about a gym membership",
|
||||
"Discussing the menu options at a fast-food restaurant",
|
||||
"Talking to a neighbor about a community event",
|
||||
"Asking for help with computer problems",
|
||||
"Discussing a recent sports game with a sports enthusiast",
|
||||
"Talking to a pet store employee about buying a pet",
|
||||
"Asking for information about a local farmer's market",
|
||||
"Discussing the details of a home renovation project",
|
||||
"Talking to a coworker about office supplies",
|
||||
"Making plans for a family picnic",
|
||||
"Inquiring about admission requirements at a university",
|
||||
"Discussing the features of a new smartphone with a salesperson",
|
||||
"Talking to a mechanic about car repairs",
|
||||
"Making arrangements for a child's birthday party",
|
||||
"Discussing a new diet plan with a nutritionist",
|
||||
"Asking for information about a music concert",
|
||||
"Talking to a hairdresser about getting a haircut",
|
||||
"Inquiring about a language course at a language school",
|
||||
"Discussing plans for a weekend camping trip",
|
||||
"Talking to a bank teller about opening a new account",
|
||||
"Ordering a drink at a coffee shop",
|
||||
"Discussing a new book with a book club member",
|
||||
"Talking to a librarian about library services",
|
||||
"Asking for advice on finding a job",
|
||||
"Discussing plans for a garden makeover with a landscaper",
|
||||
"Talking to a travel agent about a cruise vacation",
|
||||
"Inquiring about a fitness class at a gym",
|
||||
"Ordering flowers for a special occasion",
|
||||
"Discussing a new exercise routine with a personal trainer",
|
||||
"Talking to a teacher about a child's progress in school",
|
||||
"Asking for information about a local art exhibition",
|
||||
"Discussing a home improvement project with a contractor",
|
||||
"Talking to a babysitter about childcare arrangements",
|
||||
"Making arrangements for a car service appointment",
|
||||
"Inquiring about a photography workshop at a studio",
|
||||
"Discussing plans for a family reunion with a relative",
|
||||
"Talking to a tech support representative about computer issues",
|
||||
"Asking for recommendations on pet grooming services",
|
||||
"Discussing weekend plans with a significant other",
|
||||
"Talking to a counselor about personal issues",
|
||||
"Inquiring about a music lesson with a music teacher",
|
||||
"Ordering a pizza for delivery",
|
||||
"Making a reservation for a taxi",
|
||||
"Discussing a new recipe with a chef",
|
||||
"Talking to a fitness trainer about weight loss goals",
|
||||
"Inquiring about a dance class at a dance studio",
|
||||
"Ordering a meal at a food truck",
|
||||
"Discussing plans for a weekend getaway with a partner",
|
||||
"Talking to a florist about wedding flower arrangements",
|
||||
"Asking for advice on home decorating",
|
||||
"Discussing plans for a charity fundraiser event",
|
||||
"Talking to a pet sitter about taking care of pets",
|
||||
"Making arrangements for a spa day with a friend",
|
||||
"Asking for recommendations on home improvement stores",
|
||||
"Discussing weekend plans with a travel enthusiast",
|
||||
"Talking to a car mechanic about car maintenance",
|
||||
"Inquiring about a cooking class at a culinary school",
|
||||
"Ordering a sandwich at a deli",
|
||||
"Discussing plans for a family holiday party",
|
||||
"Talking to a personal assistant about organizing tasks",
|
||||
"Asking for information about a local theater production",
|
||||
"Discussing a new DIY project with a home improvement expert",
|
||||
"Talking to a wine expert about wine pairing",
|
||||
"Making arrangements for a pet adoption",
|
||||
"Asking for advice on planning a wedding"
|
||||
]
|
||||
|
||||
SOCIAL_MONOLOGUE_CONTEXTS = [
|
||||
"A guided tour of a historical museum",
|
||||
"An introduction to a new city for tourists",
|
||||
"An orientation session for new university students",
|
||||
"A safety briefing for airline passengers",
|
||||
"An explanation of the process of recycling",
|
||||
"A lecture on the benefits of a healthy diet",
|
||||
"A talk on the importance of time management",
|
||||
"A monologue about wildlife conservation",
|
||||
"An overview of local public transportation options",
|
||||
"A presentation on the history of cinema",
|
||||
"An introduction to the art of photography",
|
||||
"A discussion about the effects of climate change",
|
||||
"An overview of different types of cuisine",
|
||||
"A lecture on the principles of financial planning",
|
||||
"A monologue about sustainable energy sources",
|
||||
"An explanation of the process of online shopping",
|
||||
"A guided tour of a botanical garden",
|
||||
"An introduction to a local wildlife sanctuary",
|
||||
"A safety briefing for hikers in a national park",
|
||||
"A talk on the benefits of physical exercise",
|
||||
"A lecture on the principles of effective communication",
|
||||
"A monologue about the impact of social media",
|
||||
"An overview of the history of a famous landmark",
|
||||
"An introduction to the world of fashion design",
|
||||
"A discussion about the challenges of global poverty",
|
||||
"An explanation of the process of organic farming",
|
||||
"A presentation on the history of space exploration",
|
||||
"An overview of traditional music from different cultures",
|
||||
"A lecture on the principles of effective leadership",
|
||||
"A monologue about the influence of technology",
|
||||
"A guided tour of a famous archaeological site",
|
||||
"An introduction to a local wildlife rehabilitation center",
|
||||
"A safety briefing for visitors to a science museum",
|
||||
"A talk on the benefits of learning a new language",
|
||||
"A lecture on the principles of architectural design",
|
||||
"A monologue about the impact of renewable energy",
|
||||
"An explanation of the process of online banking",
|
||||
"A presentation on the history of a famous art movement",
|
||||
"An overview of traditional clothing from various regions",
|
||||
"A lecture on the principles of sustainable agriculture",
|
||||
"A discussion about the challenges of urban development",
|
||||
"A monologue about the influence of social norms",
|
||||
"A guided tour of a historical battlefield",
|
||||
"An introduction to a local animal shelter",
|
||||
"A safety briefing for participants in a charity run",
|
||||
"A talk on the benefits of community involvement",
|
||||
"A lecture on the principles of sustainable tourism",
|
||||
"A monologue about the impact of alternative medicine",
|
||||
"An explanation of the process of wildlife tracking",
|
||||
"A presentation on the history of a famous inventor",
|
||||
"An overview of traditional dance forms from different cultures",
|
||||
"A lecture on the principles of ethical business practices",
|
||||
"A discussion about the challenges of healthcare access",
|
||||
"A monologue about the influence of cultural traditions",
|
||||
"A guided tour of a famous lighthouse",
|
||||
"An introduction to a local astronomy observatory",
|
||||
"A safety briefing for participants in a team-building event",
|
||||
"A talk on the benefits of volunteering",
|
||||
"A lecture on the principles of wildlife protection",
|
||||
"A monologue about the impact of space exploration",
|
||||
"An explanation of the process of wildlife photography",
|
||||
"A presentation on the history of a famous musician",
|
||||
"An overview of traditional art forms from different cultures",
|
||||
"A lecture on the principles of effective education",
|
||||
"A discussion about the challenges of sustainable development",
|
||||
"A monologue about the influence of cultural diversity",
|
||||
"A guided tour of a famous national park",
|
||||
"An introduction to a local marine conservation project",
|
||||
"A safety briefing for participants in a hot air balloon ride",
|
||||
"A talk on the benefits of cultural exchange programs",
|
||||
"A lecture on the principles of wildlife conservation",
|
||||
"A monologue about the impact of technological advancements",
|
||||
"An explanation of the process of wildlife rehabilitation",
|
||||
"A presentation on the history of a famous explorer",
|
||||
"A lecture on the principles of effective marketing",
|
||||
"A discussion about the challenges of environmental sustainability",
|
||||
"A monologue about the influence of social entrepreneurship",
|
||||
"A guided tour of a famous historical estate",
|
||||
"An introduction to a local marine life research center",
|
||||
"A safety briefing for participants in a zip-lining adventure",
|
||||
"A talk on the benefits of cultural preservation",
|
||||
"A lecture on the principles of wildlife ecology",
|
||||
"A monologue about the impact of space technology",
|
||||
"An explanation of the process of wildlife conservation",
|
||||
"A presentation on the history of a famous scientist",
|
||||
"An overview of traditional crafts and artisans from different cultures",
|
||||
"A lecture on the principles of effective intercultural communication"
|
||||
]
|
||||
|
||||
FOUR_PEOPLE_SCENARIOS = [
|
||||
"A university lecture on history",
|
||||
"A physics class discussing Newton's laws",
|
||||
"A medical school seminar on anatomy",
|
||||
"A training session on computer programming",
|
||||
"A business school lecture on marketing strategies",
|
||||
"A chemistry lab experiment and discussion",
|
||||
"A language class practicing conversational skills",
|
||||
"A workshop on creative writing techniques",
|
||||
"A high school math lesson on calculus",
|
||||
"A training program for customer service representatives",
|
||||
"A lecture on environmental science and sustainability",
|
||||
"A psychology class exploring human behavior",
|
||||
"A music theory class analyzing compositions",
|
||||
"A nursing school simulation for patient care",
|
||||
"A computer science class on algorithms",
|
||||
"A workshop on graphic design principles",
|
||||
"A law school lecture on constitutional law",
|
||||
"A geology class studying rock formations",
|
||||
"A vocational training program for electricians",
|
||||
"A history seminar focusing on ancient civilizations",
|
||||
"A biology class dissecting specimens",
|
||||
"A financial literacy course for adults",
|
||||
"A literature class discussing classic novels",
|
||||
"A training session for emergency response teams",
|
||||
"A sociology lecture on social inequality",
|
||||
"An art class exploring different painting techniques",
|
||||
"A medical school seminar on diagnosis",
|
||||
"A programming bootcamp teaching web development",
|
||||
"An economics class analyzing market trends",
|
||||
"A chemistry lab experiment on chemical reactions",
|
||||
"A language class practicing pronunciation",
|
||||
"A workshop on public speaking skills",
|
||||
"A high school physics lesson on electromagnetism",
|
||||
"A training program for IT professionals",
|
||||
"A lecture on climate change and its effects",
|
||||
"A psychology class studying cognitive psychology",
|
||||
"A music class composing original songs",
|
||||
"A nursing school simulation for patient assessment",
|
||||
"A computer science class on data structures",
|
||||
"A workshop on 3D modeling and animation",
|
||||
"A law school lecture on contract law",
|
||||
"A geography class examining world maps",
|
||||
"A vocational training program for plumbers",
|
||||
"A history seminar discussing revolutions",
|
||||
"A biology class exploring genetics",
|
||||
"A financial literacy course for teens",
|
||||
"A literature class analyzing poetry",
|
||||
"A training session for public speaking coaches",
|
||||
"A sociology lecture on cultural diversity",
|
||||
"An art class creating sculptures",
|
||||
"A medical school seminar on surgical techniques",
|
||||
"A programming bootcamp teaching app development",
|
||||
"An economics class on global trade policies",
|
||||
"A chemistry lab experiment on chemical bonding",
|
||||
"A language class discussing idiomatic expressions",
|
||||
"A workshop on conflict resolution",
|
||||
"A high school biology lesson on evolution",
|
||||
"A training program for project managers",
|
||||
"A lecture on renewable energy sources",
|
||||
"A psychology class on abnormal psychology",
|
||||
"A music class rehearsing for a performance",
|
||||
"A nursing school simulation for emergency response",
|
||||
"A computer science class on cybersecurity",
|
||||
"A workshop on digital marketing strategies",
|
||||
"A law school lecture on intellectual property",
|
||||
"A geology class analyzing seismic activity",
|
||||
"A vocational training program for carpenters",
|
||||
"A history seminar on the Renaissance",
|
||||
"A chemistry class synthesizing compounds",
|
||||
"A financial literacy course for seniors",
|
||||
"A literature class interpreting Shakespearean plays",
|
||||
"A training session for negotiation skills",
|
||||
"A sociology lecture on urbanization",
|
||||
"An art class creating digital art",
|
||||
"A medical school seminar on patient communication",
|
||||
"A programming bootcamp teaching mobile app development",
|
||||
"An economics class on fiscal policy",
|
||||
"A physics lab experiment on electromagnetism",
|
||||
"A language class on cultural immersion",
|
||||
"A workshop on time management",
|
||||
"A high school chemistry lesson on stoichiometry",
|
||||
"A training program for HR professionals",
|
||||
"A lecture on space exploration and astronomy",
|
||||
"A psychology class on human development",
|
||||
"A music class practicing for a recital",
|
||||
"A nursing school simulation for triage",
|
||||
"A computer science class on web development frameworks",
|
||||
"A workshop on team-building exercises",
|
||||
"A law school lecture on criminal law",
|
||||
"A geography class studying world cultures",
|
||||
"A vocational training program for HVAC technicians",
|
||||
"A history seminar on ancient civilizations",
|
||||
"A biology class examining ecosystems",
|
||||
"A financial literacy course for entrepreneurs",
|
||||
"A literature class analyzing modern literature",
|
||||
"A training session for leadership skills",
|
||||
"A sociology lecture on gender studies",
|
||||
"An art class exploring multimedia art",
|
||||
"A medical school seminar on patient diagnosis",
|
||||
"A programming bootcamp teaching software architecture"
|
||||
]
|
||||
|
||||
ACADEMIC_SUBJECTS = [
|
||||
"Astrophysics",
|
||||
"Microbiology",
|
||||
"Political Science",
|
||||
"Environmental Science",
|
||||
"Literature",
|
||||
"Biochemistry",
|
||||
"Sociology",
|
||||
"Art History",
|
||||
"Geology",
|
||||
"Economics",
|
||||
"Psychology",
|
||||
"History of Architecture",
|
||||
"Linguistics",
|
||||
"Neurobiology",
|
||||
"Anthropology",
|
||||
"Quantum Mechanics",
|
||||
"Urban Planning",
|
||||
"Philosophy",
|
||||
"Marine Biology",
|
||||
"International Relations",
|
||||
"Medieval History",
|
||||
"Geophysics",
|
||||
"Finance",
|
||||
"Educational Psychology",
|
||||
"Graphic Design",
|
||||
"Paleontology",
|
||||
"Macroeconomics",
|
||||
"Cognitive Psychology",
|
||||
"Renaissance Art",
|
||||
"Archaeology",
|
||||
"Microeconomics",
|
||||
"Social Psychology",
|
||||
"Contemporary Art",
|
||||
"Meteorology",
|
||||
"Political Philosophy",
|
||||
"Space Exploration",
|
||||
"Cognitive Science",
|
||||
"Classical Music",
|
||||
"Oceanography",
|
||||
"Public Health",
|
||||
"Gender Studies",
|
||||
"Baroque Art",
|
||||
"Volcanology",
|
||||
"Business Ethics",
|
||||
"Music Composition",
|
||||
"Environmental Policy",
|
||||
"Media Studies",
|
||||
"Ancient History",
|
||||
"Seismology",
|
||||
"Marketing",
|
||||
"Human Development",
|
||||
"Modern Art",
|
||||
"Astronomy",
|
||||
"International Law",
|
||||
"Developmental Psychology",
|
||||
"Film Studies",
|
||||
"American History",
|
||||
"Soil Science",
|
||||
"Entrepreneurship",
|
||||
"Clinical Psychology",
|
||||
"Contemporary Dance",
|
||||
"Space Physics",
|
||||
"Political Economy",
|
||||
"Cognitive Neuroscience",
|
||||
"20th Century Literature",
|
||||
"Public Administration",
|
||||
"European History",
|
||||
"Atmospheric Science",
|
||||
"Supply Chain Management",
|
||||
"Social Work",
|
||||
"Japanese Literature",
|
||||
"Planetary Science",
|
||||
"Labor Economics",
|
||||
"Industrial-Organizational Psychology",
|
||||
"French Philosophy",
|
||||
"Biogeochemistry",
|
||||
"Strategic Management",
|
||||
"Educational Sociology",
|
||||
"Postmodern Literature",
|
||||
"Public Relations",
|
||||
"Middle Eastern History",
|
||||
"Oceanography",
|
||||
"International Development",
|
||||
"Human Resources Management",
|
||||
"Educational Leadership",
|
||||
"Russian Literature",
|
||||
"Quantum Chemistry",
|
||||
"Environmental Economics",
|
||||
"Environmental Psychology",
|
||||
"Ancient Philosophy",
|
||||
"Immunology",
|
||||
"Comparative Politics",
|
||||
"Child Development",
|
||||
"Fashion Design",
|
||||
"Geological Engineering",
|
||||
"Macroeconomic Policy",
|
||||
"Media Psychology",
|
||||
"Byzantine Art",
|
||||
"Ecology",
|
||||
"International Business"
|
||||
]
|
||||
167
ielts_be/configs/dependency_injection.py
Normal file
167
ielts_be/configs/dependency_injection.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from dependency_injector import providers, containers
|
||||
from firebase_admin import credentials
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from openai import AsyncOpenAI
|
||||
from httpx import AsyncClient as HTTPClient
|
||||
from dotenv import load_dotenv
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
from ielts_be.repositories.impl import *
|
||||
from ielts_be.services.impl import *
|
||||
from ielts_be.controllers.impl import *
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class DependencyInjector:
|
||||
|
||||
def __init__(self, polly_client: any, http_client: HTTPClient, stt: OpenAIWhisper):
|
||||
self._container = containers.DynamicContainer()
|
||||
self._polly_client = polly_client
|
||||
self._http_client = http_client
|
||||
self._stt = stt
|
||||
|
||||
def inject(self):
|
||||
self._setup_clients()
|
||||
self._setup_third_parties()
|
||||
self._setup_repositories()
|
||||
self._setup_services()
|
||||
self._setup_controllers()
|
||||
self._container.wire(
|
||||
packages=["ielts_be"]
|
||||
)
|
||||
return self
|
||||
|
||||
def _setup_clients(self):
|
||||
self._container.openai_client = providers.Singleton(AsyncOpenAI)
|
||||
self._container.polly_client = providers.Object(self._polly_client)
|
||||
self._container.http_client = providers.Object(self._http_client)
|
||||
self._container.stt = providers.Object(self._stt)
|
||||
|
||||
def _setup_third_parties(self):
|
||||
self._container.llm = providers.Factory(OpenAI, client=self._container.openai_client)
|
||||
self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client)
|
||||
|
||||
"""
|
||||
with open('ielts_be/services/impl/third_parties/elai/conf.json', 'r') as file:
|
||||
elai_conf = json.load(file)
|
||||
|
||||
with open('ielts_be/services/impl/third_parties/elai/avatars.json', 'r') as file:
|
||||
elai_avatars = json.load(file)
|
||||
"""
|
||||
|
||||
with open('ielts_be/services/impl/third_parties/heygen/avatars.json', 'r') as file:
|
||||
heygen_avatars = json.load(file)
|
||||
|
||||
self._container.vid_gen = providers.Factory(
|
||||
Heygen, client=self._container.http_client, token=os.getenv("HEY_GEN_TOKEN"), avatars=heygen_avatars
|
||||
)
|
||||
self._container.ai_detector = providers.Factory(
|
||||
GPTZero, client=self._container.http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY")
|
||||
)
|
||||
|
||||
def _setup_repositories(self):
|
||||
cred = credentials.Certificate(os.getenv("GOOGLE_APPLICATION_CREDENTIALS"))
|
||||
firebase_token = cred.get_access_token().access_token
|
||||
|
||||
self._container.document_store = providers.Factory(
|
||||
MongoDB, mongo_db=AsyncIOMotorClient(os.getenv("MONGODB_URI"))[os.getenv("MONGODB_DB")]
|
||||
)
|
||||
|
||||
self._container.firebase_instance = providers.Factory(
|
||||
FirebaseStorage,
|
||||
client=self._container.http_client, token=firebase_token, bucket=os.getenv("FIREBASE_BUCKET")
|
||||
)
|
||||
|
||||
def _setup_services(self):
|
||||
self._container.listening_service = providers.Factory(
|
||||
ListeningService,
|
||||
llm=self._container.llm,
|
||||
stt=self._container.stt,
|
||||
tts=self._container.tts,
|
||||
file_storage=self._container.firebase_instance,
|
||||
document_store=self._container.document_store
|
||||
)
|
||||
self._container.reading_service = providers.Factory(ReadingService, llm=self._container.llm)
|
||||
|
||||
self._container.speaking_service = providers.Factory(
|
||||
SpeakingService, llm=self._container.llm,
|
||||
file_storage=self._container.firebase_instance,
|
||||
stt=self._container.stt
|
||||
)
|
||||
|
||||
self._container.writing_service = providers.Factory(
|
||||
WritingService, llm=self._container.llm, ai_detector=self._container.ai_detector, file_storage=self._container.firebase_instance
|
||||
)
|
||||
|
||||
with open('ielts_be/services/impl/exam/level/mc_variants.json', 'r') as file:
|
||||
mc_variants = json.load(file)
|
||||
|
||||
self._container.level_service = providers.Factory(
|
||||
LevelService, llm=self._container.llm, document_store=self._container.document_store,
|
||||
mc_variants=mc_variants, reading_service=self._container.reading_service,
|
||||
writing_service=self._container.writing_service, speaking_service=self._container.speaking_service,
|
||||
listening_service=self._container.listening_service
|
||||
)
|
||||
|
||||
self._container.grade_service = providers.Factory(
|
||||
GradeService, llm=self._container.llm
|
||||
)
|
||||
|
||||
embeddings = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
|
||||
self._container.training_kb = providers.Factory(
|
||||
TrainingContentKnowledgeBase, embeddings=embeddings
|
||||
)
|
||||
|
||||
self._container.training_service = providers.Factory(
|
||||
TrainingService, llm=self._container.llm,
|
||||
document_store=self._container.document_store, training_kb=self._container.training_kb
|
||||
)
|
||||
|
||||
self._container.user_service = providers.Factory(
|
||||
UserService, document_store=self._container.document_store
|
||||
)
|
||||
|
||||
self._container.evaluation_service = providers.Factory(
|
||||
EvaluationService, db=self._container.document_store,
|
||||
writing_service=self._container.writing_service,
|
||||
speaking_service=self._container.speaking_service
|
||||
)
|
||||
|
||||
def _setup_controllers(self):
|
||||
|
||||
self._container.grade_controller = providers.Factory(
|
||||
GradeController, grade_service=self._container.grade_service,
|
||||
evaluation_service=self._container.evaluation_service
|
||||
)
|
||||
|
||||
self._container.user_controller = providers.Factory(
|
||||
UserController, user_service=self._container.user_service
|
||||
)
|
||||
|
||||
self._container.training_controller = providers.Factory(
|
||||
TrainingController, training_service=self._container.training_service
|
||||
)
|
||||
|
||||
self._container.level_controller = providers.Factory(
|
||||
LevelController, level_service=self._container.level_service
|
||||
)
|
||||
self._container.listening_controller = providers.Factory(
|
||||
ListeningController, listening_service=self._container.listening_service
|
||||
)
|
||||
|
||||
self._container.reading_controller = providers.Factory(
|
||||
ReadingController, reading_service=self._container.reading_service
|
||||
)
|
||||
|
||||
self._container.speaking_controller = providers.Factory(
|
||||
SpeakingController, speaking_service=self._container.speaking_service, vid_gen=self._container.vid_gen
|
||||
)
|
||||
|
||||
self._container.writing_controller = providers.Factory(
|
||||
WritingController, writing_service=self._container.writing_service
|
||||
)
|
||||
7
ielts_be/configs/logging/__init__.py
Normal file
7
ielts_be/configs/logging/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .filters import ErrorAndAboveFilter
|
||||
from .queue_handler import QueueListenerHandler
|
||||
|
||||
__all__ = [
|
||||
"ErrorAndAboveFilter",
|
||||
"QueueListenerHandler"
|
||||
]
|
||||
6
ielts_be/configs/logging/filters.py
Normal file
6
ielts_be/configs/logging/filters.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import logging
|
||||
|
||||
|
||||
class ErrorAndAboveFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord:
|
||||
return record.levelno < logging.ERROR
|
||||
105
ielts_be/configs/logging/formatters.py
Normal file
105
ielts_be/configs/logging/formatters.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
|
||||
LOG_RECORD_BUILTIN_ATTRS = {
|
||||
"args",
|
||||
"asctime",
|
||||
"created",
|
||||
"exc_info",
|
||||
"exc_text",
|
||||
"filename",
|
||||
"funcName",
|
||||
"levelname",
|
||||
"levelno",
|
||||
"lineno",
|
||||
"module",
|
||||
"msecs",
|
||||
"message",
|
||||
"msg",
|
||||
"name",
|
||||
"pathname",
|
||||
"process",
|
||||
"processName",
|
||||
"relativeCreated",
|
||||
"stack_info",
|
||||
"thread",
|
||||
"threadName",
|
||||
"taskName",
|
||||
}
|
||||
|
||||
"""
|
||||
This isn't being used since the app will be run on gcloud run but this can be used for future apps.
|
||||
If you want to test it:
|
||||
|
||||
formatters:
|
||||
|
||||
"json": {
|
||||
"()": "json_formatter.JSONFormatter",
|
||||
"fmt_keys": {
|
||||
"level": "levelname",
|
||||
"message": "message",
|
||||
"timestamp": "timestamp",
|
||||
"logger": "name",
|
||||
"module": "module",
|
||||
"function": "funcName",
|
||||
"line": "lineno",
|
||||
"thread_name": "threadName"
|
||||
}
|
||||
}
|
||||
|
||||
handlers:
|
||||
|
||||
"file_json": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "json",
|
||||
"filename": "logs/log",
|
||||
"maxBytes": 1000000,
|
||||
"backupCount": 3
|
||||
}
|
||||
|
||||
and add "cfg://handlers.file_json" to queue handler
|
||||
"""
|
||||
|
||||
# From this video https://www.youtube.com/watch?v=9L77QExPmI0
|
||||
# Src here: https://github.com/mCodingLLC/VideosSampleCode/blob/master/videos/135_modern_logging/mylogger.py
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
fmt_keys: dict[str, str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.fmt_keys = fmt_keys if fmt_keys is not None else {}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
message = self._prepare_log_dict(record)
|
||||
return json.dumps(message, default=str)
|
||||
|
||||
def _prepare_log_dict(self, record: logging.LogRecord):
|
||||
always_fields = {
|
||||
"message": record.getMessage(),
|
||||
"timestamp": dt.datetime.fromtimestamp(
|
||||
record.created, tz=dt.timezone.utc
|
||||
).isoformat(),
|
||||
}
|
||||
if record.exc_info is not None:
|
||||
always_fields["exc_info"] = self.formatException(record.exc_info)
|
||||
|
||||
if record.stack_info is not None:
|
||||
always_fields["stack_info"] = self.formatStack(record.stack_info)
|
||||
|
||||
message = {
|
||||
key: msg_val
|
||||
if (msg_val := always_fields.pop(val, None)) is not None
|
||||
else getattr(record, val)
|
||||
for key, val in self.fmt_keys.items()
|
||||
}
|
||||
message.update(always_fields)
|
||||
|
||||
for key, val in record.__dict__.items():
|
||||
if key not in LOG_RECORD_BUILTIN_ATTRS:
|
||||
message[key] = val
|
||||
|
||||
return message
|
||||
53
ielts_be/configs/logging/logging_config.json
Normal file
53
ielts_be/configs/logging/logging_config.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"version": 1,
|
||||
"objects": {
|
||||
"queue": {
|
||||
"class": "queue.Queue",
|
||||
"maxsize": 1000
|
||||
}
|
||||
},
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "[%(levelname)s] (%(module)s|L: %(lineno)d) %(asctime)s: %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"error_and_above": {
|
||||
"()": "ielts_be.configs.logging.ErrorAndAboveFilter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["error_and_above"]
|
||||
},
|
||||
"error": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "ERROR",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"queue_handler": {
|
||||
"class": "ielts_be.configs.logging.QueueListenerHandler",
|
||||
"handlers": [
|
||||
"cfg://handlers.console",
|
||||
"cfg://handlers.error"
|
||||
],
|
||||
"queue": "cfg://objects.queue",
|
||||
"respect_handler_level": true
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": [
|
||||
"queue_handler"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
61
ielts_be/configs/logging/queue_handler.py
Normal file
61
ielts_be/configs/logging/queue_handler.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from logging.config import ConvertingList, ConvertingDict, valid_ident
|
||||
from logging.handlers import QueueHandler, QueueListener
|
||||
from queue import Queue
|
||||
import atexit
|
||||
|
||||
|
||||
class QueueHandlerHelper:
|
||||
|
||||
@staticmethod
|
||||
def resolve_handlers(l):
|
||||
if not isinstance(l, ConvertingList):
|
||||
return l
|
||||
|
||||
# Indexing the list performs the evaluation.
|
||||
return [l[i] for i in range(len(l))]
|
||||
|
||||
@staticmethod
|
||||
def resolve_queue(q):
|
||||
if not isinstance(q, ConvertingDict):
|
||||
return q
|
||||
if '__resolved_value__' in q:
|
||||
return q['__resolved_value__']
|
||||
|
||||
cname = q.pop('class')
|
||||
klass = q.configurator.resolve(cname)
|
||||
props = q.pop('.', None)
|
||||
kwargs = {k: q[k] for k in q if valid_ident(k)}
|
||||
result = klass(**kwargs)
|
||||
if props:
|
||||
for name, value in props.items():
|
||||
setattr(result, name, value)
|
||||
|
||||
q['__resolved_value__'] = result
|
||||
return result
|
||||
|
||||
|
||||
# The guy from this video https://www.youtube.com/watch?v=9L77QExPmI0 is using logging features only available in 3.12
|
||||
# This article had the class required to build the queue handler in 3.11
|
||||
# https://rob-blackbourn.medium.com/how-to-use-python-logging-queuehandler-with-dictconfig-1e8b1284e27a
|
||||
class QueueListenerHandler(QueueHandler):
|
||||
|
||||
def __init__(self, handlers, respect_handler_level=False, auto_run=True, queue=Queue(-1)):
|
||||
queue = QueueHandlerHelper.resolve_queue(queue)
|
||||
super().__init__(queue)
|
||||
handlers = QueueHandlerHelper.resolve_handlers(handlers)
|
||||
self._listener = QueueListener(
|
||||
self.queue,
|
||||
*handlers,
|
||||
respect_handler_level=respect_handler_level)
|
||||
if auto_run:
|
||||
self.start()
|
||||
atexit.register(self.stop)
|
||||
|
||||
def start(self):
|
||||
self._listener.start()
|
||||
|
||||
def stop(self):
|
||||
self._listener.stop()
|
||||
|
||||
def emit(self, record):
|
||||
return super().emit(record)
|
||||
1275
ielts_be/configs/question_templates.py
Normal file
1275
ielts_be/configs/question_templates.py
Normal file
File diff suppressed because it is too large
Load Diff
3
ielts_be/controllers/__init__.py
Normal file
3
ielts_be/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .abc import *
|
||||
|
||||
__all__ = abc.__all__
|
||||
11
ielts_be/controllers/abc/__init__.py
Normal file
11
ielts_be/controllers/abc/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .grade import IGradeController
|
||||
from .training import ITrainingController
|
||||
from .user import IUserController
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"IGradeController",
|
||||
"ITrainingController",
|
||||
"IUserController",
|
||||
]
|
||||
__all__.extend(exam.__all__)
|
||||
13
ielts_be/controllers/abc/exam/__init__.py
Normal file
13
ielts_be/controllers/abc/exam/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .level import ILevelController
|
||||
from .listening import IListeningController
|
||||
from .reading import IReadingController
|
||||
from .writing import IWritingController
|
||||
from .speaking import ISpeakingController
|
||||
|
||||
__all__ = [
|
||||
"IListeningController",
|
||||
"IReadingController",
|
||||
"IWritingController",
|
||||
"ISpeakingController",
|
||||
"ILevelController",
|
||||
]
|
||||
27
ielts_be/controllers/abc/exam/level.py
Normal file
27
ielts_be/controllers/abc/exam/level.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class ILevelController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def generate_exercises(self, dto):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, file: UploadFile, solutions: Optional[UploadFile] = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
30
ielts_be/controllers/abc/exam/listening.py
Normal file
30
ielts_be/controllers/abc/exam/listening.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
class IListeningController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_listening_question(self, dto):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_mp3(self, dto):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def transcribe_dialog(self, audio: UploadFile):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_instructions(self, text: str):
|
||||
pass
|
||||
20
ielts_be/controllers/abc/exam/reading.py
Normal file
20
ielts_be/controllers/abc/exam/reading.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
class IReadingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, passage: int, topic: Optional[str], word_count: Optional[int]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_exercises(self, dto):
|
||||
pass
|
||||
|
||||
20
ielts_be/controllers/abc/exam/speaking.py
Normal file
20
ielts_be/controllers/abc/exam/speaking.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeakingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_avatars(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(self, text: str, avatar: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def poll_video(self, vid_id: str):
|
||||
pass
|
||||
14
ielts_be/controllers/abc/exam/writing.py
Normal file
14
ielts_be/controllers/abc/exam/writing.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi.datastructures import UploadFile
|
||||
|
||||
|
||||
class IWritingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
|
||||
pass
|
||||
30
ielts_be/controllers/abc/grade.py
Normal file
30
ielts_be/controllers/abc/grade.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict
|
||||
from fastapi import BackgroundTasks
|
||||
from fastapi.datastructures import FormData
|
||||
|
||||
|
||||
class IGradeController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(
|
||||
self,
|
||||
task: int, dto: any,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(
|
||||
self, task: int, form: FormData, background_tasks: BackgroundTasks
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grading_summary(self, data: Dict):
|
||||
pass
|
||||
|
||||
12
ielts_be/controllers/abc/training.py
Normal file
12
ielts_be/controllers/abc/training.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ITrainingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, data):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, data):
|
||||
pass
|
||||
8
ielts_be/controllers/abc/user.py
Normal file
8
ielts_be/controllers/abc/user.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IUserController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def batch_import(self, batch):
|
||||
pass
|
||||
12
ielts_be/controllers/impl/__init__.py
Normal file
12
ielts_be/controllers/impl/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .training import TrainingController
|
||||
from .grade import GradeController
|
||||
from .user import UserController
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"TrainingController",
|
||||
"GradeController",
|
||||
"UserController"
|
||||
]
|
||||
|
||||
__all__.extend(exam.__all__)
|
||||
13
ielts_be/controllers/impl/exam/__init__.py
Normal file
13
ielts_be/controllers/impl/exam/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .level import LevelController
|
||||
from .listening import ListeningController
|
||||
from .reading import ReadingController
|
||||
from .speaking import SpeakingController
|
||||
from .writing import WritingController
|
||||
|
||||
__all__ = [
|
||||
"LevelController",
|
||||
"ListeningController",
|
||||
"ReadingController",
|
||||
"SpeakingController",
|
||||
"WritingController",
|
||||
]
|
||||
26
ielts_be/controllers/impl/exam/level.py
Normal file
26
ielts_be/controllers/impl/exam/level.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict, Optional
|
||||
|
||||
from ielts_be.controllers import ILevelController
|
||||
from ielts_be.services import ILevelService
|
||||
|
||||
|
||||
class LevelController(ILevelController):
|
||||
|
||||
def __init__(self, level_service: ILevelService):
|
||||
self._service = level_service
|
||||
|
||||
async def generate_exercises(self, dto):
|
||||
return await self._service.generate_exercises(dto)
|
||||
|
||||
async def get_level_exam(self):
|
||||
return await self._service.get_level_exam()
|
||||
|
||||
async def get_level_utas(self):
|
||||
return await self._service.get_level_utas()
|
||||
|
||||
async def upload_level(self, exercises: UploadFile, solutions: Optional[UploadFile] = None):
|
||||
return await self._service.upload_level(exercises, solutions)
|
||||
|
||||
async def get_custom_level(self, data: Dict):
|
||||
return await self._service.get_custom_level(data)
|
||||
53
ielts_be/controllers/impl/exam/listening.py
Normal file
53
ielts_be/controllers/impl/exam/listening.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import io
|
||||
|
||||
from fastapi import UploadFile
|
||||
from fastapi.responses import StreamingResponse, Response
|
||||
|
||||
from ielts_be.controllers import IListeningController
|
||||
from ielts_be.services import IListeningService
|
||||
from ielts_be.dtos.listening import ListeningExercisesDTO, Dialog
|
||||
|
||||
|
||||
class ListeningController(IListeningController):
|
||||
|
||||
def __init__(self, listening_service: IListeningService):
|
||||
self._service = listening_service
|
||||
|
||||
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
|
||||
res = await self._service.import_exam(exercises, solutions)
|
||||
if not res:
|
||||
return Response(status_code=500)
|
||||
else:
|
||||
return res
|
||||
|
||||
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
|
||||
return await self._service.generate_listening_dialog(section_id, topic, difficulty)
|
||||
|
||||
async def get_listening_question(self, dto: ListeningExercisesDTO):
|
||||
return await self._service.get_listening_question(dto)
|
||||
|
||||
async def generate_mp3(self, dto: Dialog):
|
||||
mp3 = await self._service.generate_mp3(dto)
|
||||
return self._mp3_response(mp3)
|
||||
|
||||
async def create_instructions(self, text: str):
|
||||
mp3 = await self._service.create_instructions(text)
|
||||
return self._mp3_response(mp3)
|
||||
|
||||
async def transcribe_dialog(self, audio: UploadFile):
|
||||
dialog = await self._service.transcribe_dialog(audio)
|
||||
if dialog is None:
|
||||
return Response(status_code=500)
|
||||
|
||||
return dialog
|
||||
|
||||
@staticmethod
|
||||
def _mp3_response(mp3: bytes):
|
||||
return StreamingResponse(
|
||||
content=io.BytesIO(mp3),
|
||||
media_type="audio/mpeg",
|
||||
headers={
|
||||
"Content-Type": "audio/mpeg",
|
||||
"Content-Disposition": "attachment;filename=speech.mp3"
|
||||
}
|
||||
)
|
||||
28
ielts_be/controllers/impl/exam/reading.py
Normal file
28
ielts_be/controllers/impl/exam/reading.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import UploadFile, Response
|
||||
|
||||
from ielts_be.controllers import IReadingController
|
||||
from ielts_be.services import IReadingService
|
||||
from ielts_be.dtos.reading import ReadingDTO
|
||||
|
||||
|
||||
class ReadingController(IReadingController):
|
||||
|
||||
def __init__(self, reading_service: IReadingService):
|
||||
self._service = reading_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
|
||||
res = await self._service.import_exam(exercises, solutions)
|
||||
if not res:
|
||||
return Response(status_code=500)
|
||||
else:
|
||||
return res
|
||||
|
||||
async def generate_reading_passage(self, passage: int, topic: Optional[str], word_count: Optional[int]):
|
||||
return await self._service.generate_reading_passage(passage, topic, word_count)
|
||||
|
||||
async def generate_reading_exercises(self, dto: ReadingDTO):
|
||||
return await self._service.generate_reading_exercises(dto)
|
||||
24
ielts_be/controllers/impl/exam/speaking.py
Normal file
24
ielts_be/controllers/impl/exam/speaking.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
|
||||
from ielts_be.controllers import ISpeakingController
|
||||
from ielts_be.services import ISpeakingService, IVideoGeneratorService
|
||||
|
||||
|
||||
class SpeakingController(ISpeakingController):
|
||||
|
||||
def __init__(self, speaking_service: ISpeakingService, vid_gen: IVideoGeneratorService):
|
||||
self._service = speaking_service
|
||||
self._vid_gen = vid_gen
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
|
||||
return await self._service.get_speaking_part(task, topic, second_topic, difficulty)
|
||||
|
||||
async def get_avatars(self):
|
||||
return await self._vid_gen.get_avatars()
|
||||
|
||||
async def generate_video(self, text: str, avatar: str):
|
||||
return await self._vid_gen.create_video(text, avatar)
|
||||
|
||||
async def poll_video(self, vid_id: str):
|
||||
return await self._vid_gen.poll_status(vid_id)
|
||||
18
ielts_be/controllers/impl/exam/writing.py
Normal file
18
ielts_be/controllers/impl/exam/writing.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import UploadFile, HTTPException
|
||||
|
||||
from ielts_be.controllers import IWritingController
|
||||
from ielts_be.services import IWritingService
|
||||
|
||||
|
||||
class WritingController(IWritingController):
|
||||
|
||||
def __init__(self, writing_service: IWritingService):
|
||||
self._service = writing_service
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
return await self._service.get_writing_task_general_question(task, topic, difficulty)
|
||||
|
||||
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
|
||||
if attachment.content_type not in ['image/jpeg', 'image/png']:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG and PNG allowed.")
|
||||
return await self._service.get_writing_task_academic_question(task, attachment, difficulty)
|
||||
105
ielts_be/controllers/impl/grade.py
Normal file
105
ielts_be/controllers/impl/grade.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import BackgroundTasks, Response, HTTPException
|
||||
from fastapi.datastructures import FormData
|
||||
|
||||
from ielts_be.controllers import IGradeController
|
||||
from ielts_be.services import IGradeService, IEvaluationService
|
||||
from ielts_be.dtos.evaluation import EvaluationType
|
||||
from ielts_be.dtos.speaking import GradeSpeakingItem
|
||||
from ielts_be.dtos.writing import WritingGradeTaskDTO
|
||||
|
||||
class GradeController(IGradeController):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grade_service: IGradeService,
|
||||
evaluation_service: IEvaluationService,
|
||||
):
|
||||
self._service = grade_service
|
||||
self._evaluation_service = evaluation_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def grade_writing_task(
|
||||
self,
|
||||
task: int, dto: WritingGradeTaskDTO, background_tasks: BackgroundTasks
|
||||
):
|
||||
await self._evaluation_service.begin_evaluation(
|
||||
dto.userId, dto.sessionId, task, dto.exerciseId, EvaluationType.WRITING, dto, background_tasks
|
||||
)
|
||||
|
||||
return Response(status_code=200)
|
||||
|
||||
async def grade_speaking_task(self, task: int, form: FormData, background_tasks: BackgroundTasks):
|
||||
answers: Dict[int, Dict] = {}
|
||||
user_id = form.get("userId")
|
||||
session_id = form.get("sessionId")
|
||||
exercise_id = form.get("exerciseId")
|
||||
|
||||
if not session_id or not exercise_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Fields sessionId and exerciseId are required!"
|
||||
)
|
||||
|
||||
for key, value in form.items():
|
||||
if '_' not in key:
|
||||
continue
|
||||
|
||||
field_name, index = key.rsplit('_', 1)
|
||||
index = int(index)
|
||||
|
||||
if index not in answers:
|
||||
answers[index] = {}
|
||||
|
||||
if field_name == 'question':
|
||||
answers[index]['question'] = value
|
||||
elif field_name == 'audio':
|
||||
answers[index]['answer'] = value
|
||||
|
||||
for i, answer in answers.items():
|
||||
if 'question' not in answer or 'answer' not in answer:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Incomplete data for answer {i}. Both question and audio required."
|
||||
)
|
||||
|
||||
items = [
|
||||
GradeSpeakingItem(
|
||||
question=answers[i]['question'],
|
||||
answer=answers[i]['answer']
|
||||
)
|
||||
for i in sorted(answers.keys())
|
||||
]
|
||||
|
||||
ex_type = EvaluationType.SPEAKING if task == 2 else EvaluationType.SPEAKING_INTERACTIVE
|
||||
|
||||
await self._evaluation_service.begin_evaluation(
|
||||
user_id, session_id, task, exercise_id, ex_type, items, background_tasks
|
||||
)
|
||||
|
||||
return Response(status_code=200)
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
return await self._service.grade_short_answers(data)
|
||||
|
||||
async def grading_summary(self, data: Dict):
|
||||
section_keys = ['reading', 'listening', 'writing', 'speaking', 'level']
|
||||
extracted_sections = self._extract_existing_sections_from_body(data, section_keys)
|
||||
return await self._service.calculate_grading_summary(extracted_sections)
|
||||
|
||||
@staticmethod
|
||||
def _extract_existing_sections_from_body(my_dict, keys_to_extract):
|
||||
if 'sections' in my_dict and isinstance(my_dict['sections'], list) and len(my_dict['sections']) > 0:
|
||||
return list(
|
||||
filter(
|
||||
lambda item:
|
||||
'code' in item and
|
||||
item['code'] in keys_to_extract and
|
||||
'grade' in item and
|
||||
'name' in item,
|
||||
my_dict['sections']
|
||||
)
|
||||
)
|
||||
|
||||
17
ielts_be/controllers/impl/training.py
Normal file
17
ielts_be/controllers/impl/training.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Dict
|
||||
|
||||
from ielts_be.controllers import ITrainingController
|
||||
from ielts_be.services import ITrainingService
|
||||
from ielts_be.dtos.training import FetchTipsDTO
|
||||
|
||||
|
||||
class TrainingController(ITrainingController):
|
||||
|
||||
def __init__(self, training_service: ITrainingService):
|
||||
self._service = training_service
|
||||
|
||||
async def fetch_tips(self, data: FetchTipsDTO):
|
||||
return await self._service.fetch_tips(data.context, data.question, data.answer, data.correct_answer)
|
||||
|
||||
async def get_training_content(self, data: Dict):
|
||||
return await self._service.get_training_content(data)
|
||||
12
ielts_be/controllers/impl/user.py
Normal file
12
ielts_be/controllers/impl/user.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from ielts_be.controllers import IUserController
|
||||
from ielts_be.services import IUserService
|
||||
from ielts_be.dtos.user_batch import BatchUsersDTO
|
||||
|
||||
|
||||
class UserController(IUserController):
|
||||
|
||||
def __init__(self, user_service: IUserService):
|
||||
self._service = user_service
|
||||
|
||||
async def batch_import(self, batch: BatchUsersDTO):
|
||||
return await self._service.batch_users(batch)
|
||||
0
ielts_be/dtos/__init__.py
Normal file
0
ielts_be/dtos/__init__.py
Normal file
18
ielts_be/dtos/evaluation.py
Normal file
18
ielts_be/dtos/evaluation.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class EvaluationType(str, Enum):
|
||||
WRITING = "writing"
|
||||
SPEAKING_INTERACTIVE = "speaking_interactive"
|
||||
SPEAKING = "speaking"
|
||||
|
||||
class EvaluationRecord(BaseModel):
|
||||
id: str
|
||||
session_id: str
|
||||
exercise_id: str
|
||||
type: EvaluationType
|
||||
task: int
|
||||
status: str = "pending"
|
||||
result: Optional[Dict] = None
|
||||
0
ielts_be/dtos/exams/__init__.py
Normal file
0
ielts_be/dtos/exams/__init__.py
Normal file
60
ielts_be/dtos/exams/level.py
Normal file
60
ielts_be/dtos/exams/level.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Dict, Union, Optional
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
solution: str
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class MultipleChoiceExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "multipleChoice"
|
||||
prompt: str = "Select the appropriate option."
|
||||
questions: List[MultipleChoiceQuestion]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
class FillBlanksSolution(BaseModel):
|
||||
id: str
|
||||
solution: str
|
||||
|
||||
|
||||
class FillBlanksExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "fillBlanks"
|
||||
variant: str = "mc"
|
||||
prompt: str = "Click a blank to select the appropriate word for it."
|
||||
text: str
|
||||
solutions: List[FillBlanksSolution]
|
||||
words: List[FillBlanksWord]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
Exercise = Union[MultipleChoiceExercise, FillBlanksExercise]
|
||||
|
||||
class Text(BaseModel):
|
||||
content: str
|
||||
title: str
|
||||
|
||||
class Part(BaseModel):
|
||||
exercises: List[Exercise]
|
||||
text: Optional[Text] = Field(default=None)
|
||||
|
||||
|
||||
class Exam(BaseModel):
|
||||
parts: List[Part]
|
||||
92
ielts_be/dtos/exams/listening.py
Normal file
92
ielts_be/dtos/exams/listening.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Union, Optional, Literal, Any
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from ielts_be.dtos.listening import Dialog
|
||||
|
||||
|
||||
class ExerciseBase(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str
|
||||
prompt: str
|
||||
|
||||
|
||||
class TrueFalseSolution(str, Enum):
|
||||
TRUE = "true"
|
||||
FALSE = "false"
|
||||
NOT_GIVEN = "not_given"
|
||||
|
||||
class TrueFalseQuestions(BaseModel):
|
||||
prompt: str
|
||||
solution: TrueFalseSolution
|
||||
id: str
|
||||
|
||||
|
||||
class TrueFalseExercise(ExerciseBase):
|
||||
type: Literal["trueFalse"]
|
||||
questions: List[TrueFalseQuestions]
|
||||
|
||||
|
||||
class MCOption(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MCQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
options: List[MCOption]
|
||||
solution: str
|
||||
variant: str = "text"
|
||||
|
||||
|
||||
class MultipleChoiceExercise(ExerciseBase):
|
||||
type: Literal["multipleChoice"]
|
||||
questions: List[MCQuestion]
|
||||
|
||||
|
||||
class WriteBlankQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
solution: List[str]
|
||||
|
||||
class WriteBlanksVariant(str, Enum):
|
||||
QUESTIONS = "questions"
|
||||
FILL = "fill"
|
||||
FORM = "form"
|
||||
|
||||
class WriteBlanksQuestionExercise(ExerciseBase):
|
||||
type: Literal["writeBlanks"]
|
||||
maxWords: int
|
||||
questions: List[WriteBlankQuestion]
|
||||
variant: WriteBlanksVariant
|
||||
|
||||
class WriteBlankSolution(BaseModel):
|
||||
id: str
|
||||
solution: List[str]
|
||||
|
||||
class WriteBlanksExercise(ExerciseBase):
|
||||
type: Literal["writeBlanks"]
|
||||
maxWords: int
|
||||
solutions: List[WriteBlankSolution]
|
||||
text: str
|
||||
variant: Optional[WriteBlanksVariant]
|
||||
|
||||
|
||||
ListeningExercise = Union[
|
||||
TrueFalseExercise,
|
||||
MultipleChoiceExercise,
|
||||
WriteBlanksExercise
|
||||
]
|
||||
|
||||
|
||||
class ListeningSection(BaseModel):
|
||||
exercises: List[ListeningExercise]
|
||||
script: Optional[Union[List[Any] | str]] = None
|
||||
|
||||
|
||||
class ListeningExam(BaseModel):
|
||||
module: str = "listening"
|
||||
minTimer: Optional[int]
|
||||
parts: List[ListeningSection]
|
||||
107
ielts_be/dtos/exams/reading.py
Normal file
107
ielts_be/dtos/exams/reading.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Union, Optional
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
|
||||
class WriteBlanksSolution(BaseModel):
|
||||
id: str
|
||||
solution: List[str]
|
||||
|
||||
class WriteBlanksExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "writeBlanks"
|
||||
maxWords: int
|
||||
solutions: List[WriteBlanksSolution]
|
||||
text: str
|
||||
prompt: str
|
||||
|
||||
|
||||
class MatchSentencesOption(BaseModel):
|
||||
id: str
|
||||
sentence: str
|
||||
|
||||
class MatchSentencesSentence(MatchSentencesOption):
|
||||
solution: str
|
||||
|
||||
class MatchSentencesVariant(str, Enum):
|
||||
HEADING = "heading"
|
||||
IDEAMATCH = "ideaMatch"
|
||||
|
||||
class MCOption(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
class MCQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
options: List[MCOption]
|
||||
solution: str
|
||||
variant: Optional[str] = None
|
||||
|
||||
class MultipleChoice(BaseModel):
|
||||
questions: List[MCQuestion]
|
||||
type: str
|
||||
prompt: str
|
||||
|
||||
|
||||
class MatchSentencesExercise(BaseModel):
|
||||
options: List[MatchSentencesOption]
|
||||
sentences: List[MatchSentencesSentence]
|
||||
type: str = "matchSentences"
|
||||
variant: MatchSentencesVariant
|
||||
prompt: str
|
||||
|
||||
class TrueFalseSolution(str, Enum):
|
||||
TRUE = "true"
|
||||
FALSE = "false"
|
||||
NOT_GIVEN = "not_given"
|
||||
|
||||
class TrueFalseQuestions(BaseModel):
|
||||
prompt: str
|
||||
solution: TrueFalseSolution
|
||||
id: str
|
||||
|
||||
class TrueFalseExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
questions: List[TrueFalseQuestions]
|
||||
type: str = "trueFalse"
|
||||
prompt: str = "Do the following statements agree with the information given in the Reading Passage?"
|
||||
|
||||
|
||||
|
||||
class FillBlanksSolution(BaseModel):
|
||||
id: str
|
||||
solution: str
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
letter: str
|
||||
word: str
|
||||
|
||||
class FillBlanksExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
solutions: List[FillBlanksSolution]
|
||||
text: str
|
||||
type: str = "fillBlanks"
|
||||
words: List[FillBlanksWord]
|
||||
allowRepetition: bool = False
|
||||
prompt: str
|
||||
|
||||
Exercise = Union[FillBlanksExercise, TrueFalseExercise, MatchSentencesExercise, WriteBlanksExercise, MultipleChoice]
|
||||
|
||||
|
||||
class Context(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
|
||||
class Part(BaseModel):
|
||||
exercises: List[Exercise]
|
||||
text: Context
|
||||
|
||||
class Exam(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
module: str = "reading"
|
||||
minTimer: int
|
||||
isDiagnostic: bool = False
|
||||
parts: List[Part]
|
||||
18
ielts_be/dtos/level.py
Normal file
18
ielts_be/dtos/level.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ielts_be.configs.constants import LevelExerciseType
|
||||
|
||||
|
||||
class LevelExercises(BaseModel):
|
||||
type: LevelExerciseType
|
||||
quantity: int
|
||||
text_size: Optional[int] = None
|
||||
sa_qty: Optional[int] = None
|
||||
mc_qty: Optional[int] = None
|
||||
topic: Optional[str] = None
|
||||
|
||||
class LevelExercisesDTO(BaseModel):
|
||||
exercises: List[LevelExercises]
|
||||
difficulty: Optional[str] = None
|
||||
37
ielts_be/dtos/listening.py
Normal file
37
ielts_be/dtos/listening.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import random
|
||||
import uuid
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ielts_be.configs.constants import MinTimers, EducationalContent, ListeningExerciseType
|
||||
|
||||
|
||||
class SaveListeningDTO(BaseModel):
|
||||
parts: List[Dict]
|
||||
minTimer: int = MinTimers.LISTENING_MIN_TIMER_DEFAULT
|
||||
difficulty: str = random.choice(EducationalContent.DIFFICULTIES)
|
||||
id: str = str(uuid.uuid4())
|
||||
|
||||
|
||||
class ListeningExercises(BaseModel):
|
||||
type: ListeningExerciseType
|
||||
quantity: int
|
||||
|
||||
class ListeningExercisesDTO(BaseModel):
|
||||
text: str
|
||||
exercises: List[ListeningExercises]
|
||||
difficulty: Optional[str]
|
||||
|
||||
class InstructionsDTO(BaseModel):
|
||||
text: str
|
||||
|
||||
class ConversationPayload(BaseModel):
|
||||
name: str
|
||||
gender: str
|
||||
text: str
|
||||
voice: Optional[str] = None
|
||||
|
||||
class Dialog(BaseModel):
|
||||
conversation: Optional[List[ConversationPayload]] = Field(default_factory=list)
|
||||
monologue: Optional[str] = None
|
||||
17
ielts_be/dtos/reading.py
Normal file
17
ielts_be/dtos/reading.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import random
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ielts_be.configs.constants import ReadingExerciseType, EducationalContent
|
||||
|
||||
class ReadingExercise(BaseModel):
|
||||
type: ReadingExerciseType
|
||||
quantity: int
|
||||
num_random_words: Optional[int] = Field(1)
|
||||
max_words: Optional[int] = Field(3)
|
||||
|
||||
class ReadingDTO(BaseModel):
|
||||
text: str = Field(...)
|
||||
exercises: List[ReadingExercise] = Field(...)
|
||||
difficulty: Optional[str] = None
|
||||
29
ielts_be/dtos/sheet.py
Normal file
29
ielts_be/dtos/sheet.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Union, Any, Optional
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
type: str = "multipleChoice"
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
type: str = "fillBlanks"
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
Component = Union[MultipleChoiceQuestion, FillBlanksWord, Dict[str, Any]]
|
||||
|
||||
|
||||
class Sheet(BaseModel):
|
||||
batch: Optional[int] = None
|
||||
components: List[Component]
|
||||
11
ielts_be/dtos/speaking.py
Normal file
11
ielts_be/dtos/speaking.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Video(BaseModel):
|
||||
text: str
|
||||
avatar: str
|
||||
|
||||
class GradeSpeakingItem(BaseModel):
|
||||
question: str
|
||||
answer: UploadFile
|
||||
37
ielts_be/dtos/training.py
Normal file
37
ielts_be/dtos/training.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
|
||||
class FetchTipsDTO(BaseModel):
|
||||
context: str
|
||||
question: str
|
||||
answer: str
|
||||
correct_answer: str
|
||||
|
||||
|
||||
class QueryDTO(BaseModel):
|
||||
category: str
|
||||
text: str
|
||||
|
||||
|
||||
class DetailsDTO(BaseModel):
|
||||
exam_id: str
|
||||
date: int
|
||||
performance_comment: str
|
||||
detailed_summary: str
|
||||
|
||||
|
||||
class WeakAreaDTO(BaseModel):
|
||||
area: str
|
||||
comment: str
|
||||
|
||||
|
||||
class TrainingContentDTO(BaseModel):
|
||||
details: List[DetailsDTO]
|
||||
weak_areas: List[WeakAreaDTO]
|
||||
queries: List[QueryDTO]
|
||||
|
||||
|
||||
class TipsDTO(BaseModel):
|
||||
tip_ids: List[str]
|
||||
|
||||
35
ielts_be/dtos/user_batch.py
Normal file
35
ielts_be/dtos/user_batch.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DemographicInfo(BaseModel):
|
||||
phone: str
|
||||
passport_id: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
|
||||
class Entity(BaseModel):
|
||||
id: str
|
||||
role: str
|
||||
|
||||
|
||||
class UserDTO(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
name: str
|
||||
type: str
|
||||
passport_id: str
|
||||
passwordHash: str
|
||||
passwordSalt: str
|
||||
groupName: Optional[str] = None
|
||||
corporate: Optional[str] = None
|
||||
studentID: Optional[str] = None
|
||||
expiryDate: Optional[str] = None
|
||||
demographicInformation: Optional[DemographicInfo] = None
|
||||
entities: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BatchUsersDTO(BaseModel):
|
||||
makerID: str
|
||||
users: list[UserDTO]
|
||||
14
ielts_be/dtos/video.py
Normal file
14
ielts_be/dtos/video.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class TaskStatus(Enum):
|
||||
STARTED = "STARTED"
|
||||
IN_PROGRESS = "IN_PROGRESS"
|
||||
COMPLETED = "COMPLETED"
|
||||
ERROR = "ERROR"
|
||||
|
||||
class Task(BaseModel):
|
||||
status: TaskStatus
|
||||
result: Optional[str] = None
|
||||
12
ielts_be/dtos/writing.py
Normal file
12
ielts_be/dtos/writing.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WritingGradeTaskDTO(BaseModel):
|
||||
userId: str
|
||||
sessionId: str
|
||||
exerciseId: str
|
||||
question: str
|
||||
answer: str
|
||||
attachment: Optional[str] = None
|
||||
6
ielts_be/exceptions/__init__.py
Normal file
6
ielts_be/exceptions/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .exceptions import CustomException, UnauthorizedException
|
||||
|
||||
__all__ = [
|
||||
"CustomException",
|
||||
"UnauthorizedException"
|
||||
]
|
||||
21
ielts_be/exceptions/exceptions.py
Normal file
21
ielts_be/exceptions/exceptions.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
class CustomException(Exception):
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
error_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
message = HTTPStatus.INTERNAL_SERVER_ERROR.description
|
||||
|
||||
def __init__(self, message=None):
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class UnauthorizedException(CustomException):
|
||||
code = HTTPStatus.UNAUTHORIZED
|
||||
error_code = HTTPStatus.UNAUTHORIZED
|
||||
message = HTTPStatus.UNAUTHORIZED.description
|
||||
|
||||
class TranscriptionException(CustomException):
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
error_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
11
ielts_be/helpers/__init__.py
Normal file
11
ielts_be/helpers/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .file import FileHelper
|
||||
from .text import TextHelper
|
||||
from .token_counter import count_tokens
|
||||
from .exercises import ExercisesHelper
|
||||
|
||||
__all__ = [
|
||||
"FileHelper",
|
||||
"TextHelper",
|
||||
"count_tokens",
|
||||
"ExercisesHelper",
|
||||
]
|
||||
249
ielts_be/helpers/exercises.py
Normal file
249
ielts_be/helpers/exercises.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import queue
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from wonderwords import RandomWord
|
||||
|
||||
from .text import TextHelper
|
||||
|
||||
|
||||
class ExercisesHelper:
|
||||
|
||||
@staticmethod
|
||||
def divide_number_into_parts(number, parts):
|
||||
if number < parts:
|
||||
return None
|
||||
|
||||
part_size = number // parts
|
||||
remaining = number % parts
|
||||
|
||||
q = queue.Queue()
|
||||
|
||||
for i in range(parts):
|
||||
if i < remaining:
|
||||
q.put(part_size + 1)
|
||||
else:
|
||||
q.put(part_size)
|
||||
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def fix_exercise_ids(exercise, start_id):
|
||||
# Initialize the starting ID for the first exercise
|
||||
current_id = start_id
|
||||
|
||||
questions = exercise["questions"]
|
||||
|
||||
# Iterate through questions and update the "id" value
|
||||
for question in questions:
|
||||
question["id"] = str(current_id)
|
||||
current_id += 1
|
||||
|
||||
return exercise
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders(text: str, words_to_replace: list, start_id):
|
||||
for i, word in enumerate(words_to_replace, start=start_id):
|
||||
# Create a case-insensitive regular expression pattern
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
text = pattern.sub(placeholder, text, 1)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders_notes(notes: list, words_to_replace: list, start_id):
|
||||
replaced_notes = []
|
||||
for i, note in enumerate(notes, start=0):
|
||||
word = words_to_replace[i]
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(start_id + i) + '}}'
|
||||
note = pattern.sub(placeholder, note, 1)
|
||||
replaced_notes.append(note)
|
||||
return replaced_notes
|
||||
|
||||
@staticmethod
|
||||
def add_random_words_and_shuffle(word_array, num_random_words):
|
||||
r = RandomWord()
|
||||
random_words_selected = r.random_words(num_random_words)
|
||||
|
||||
combined_array = word_array + random_words_selected
|
||||
|
||||
random.shuffle(combined_array)
|
||||
|
||||
result = []
|
||||
for i, word in enumerate(combined_array):
|
||||
letter = chr(65 + i) # chr(65) is 'A'
|
||||
result.append({"letter": letter, "word": word})
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def fillblanks_build_solutions_array(words, start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": word
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def remove_excess_questions(questions: [], quantity):
|
||||
count_true = 0
|
||||
result = []
|
||||
|
||||
for item in reversed(questions):
|
||||
if item.get('solution') == 'true' and count_true < quantity:
|
||||
count_true += 1
|
||||
else:
|
||||
result.append(item)
|
||||
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text(questions: [], start_id):
|
||||
result = ""
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
result = result + q["question"] + placeholder + "\\n"
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text_form(form: [], start_id):
|
||||
result = ""
|
||||
replaced_words = []
|
||||
for i, entry in enumerate(form, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
# Use regular expression to find the string after ':'
|
||||
match = re.search(r'(?<=:)\s*(.*)', entry)
|
||||
# Extract the matched string
|
||||
original_string = match.group(1)
|
||||
# Split the string into words
|
||||
words = re.findall(r'\b\w+\b', original_string)
|
||||
# Remove words with only one letter
|
||||
filtered_words = [word for word in words if len(word) > 1]
|
||||
# Choose a random word from the list of words
|
||||
selected_word = random.choice(filtered_words)
|
||||
pattern = re.compile(r'\b' + re.escape(selected_word) + r'\b', re.IGNORECASE)
|
||||
|
||||
# Replace the chosen word with the placeholder
|
||||
replaced_string = pattern.sub(placeholder, original_string, 1)
|
||||
# Construct the final replaced string
|
||||
replaced_string = entry.replace(original_string, replaced_string)
|
||||
|
||||
result = result + replaced_string + "\\n"
|
||||
# Save the replaced word or use it as needed
|
||||
# For example, you can save it to a file or a list
|
||||
replaced_words.append(selected_word)
|
||||
return result, replaced_words
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions(questions: [], start_id):
|
||||
solutions = []
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
solution = [q["possible_answers"]] if isinstance(q["possible_answers"], str) else q["possible_answers"]
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions_listening(words: [], start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solution = [word] if isinstance(word, str) else word
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def answer_word_limit_ok(question):
|
||||
# Check if any option in any solution has more than three words
|
||||
return not any(
|
||||
len(option.split()) > 3
|
||||
for solution in question["solutions"]
|
||||
for option in solution["solution"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assign_letters_to_paragraphs(paragraphs):
|
||||
result = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for paragraph in paragraphs.split("\n\n"):
|
||||
if TextHelper.has_x_words(paragraph, 10):
|
||||
result.append({'paragraph': paragraph.strip(), 'letter': next(letters)})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def contains_empty_dict(arr):
|
||||
return any(elem == {} for elem in arr)
|
||||
|
||||
@staticmethod
|
||||
def fix_writing_overall(overall: float, task_response: dict):
|
||||
grades = [category["grade"] for category in task_response.values()]
|
||||
|
||||
if overall > max(grades) or overall < min(grades):
|
||||
total_sum = sum(grades)
|
||||
average = total_sum / len(grades)
|
||||
rounded_average = round(average, 0)
|
||||
return rounded_average
|
||||
|
||||
return overall
|
||||
|
||||
@staticmethod
|
||||
def build_options(ideas):
|
||||
options = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
options.append({
|
||||
"id": next(letters),
|
||||
"sentence": idea["from"]
|
||||
})
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def build_sentences(ideas, start_id):
|
||||
sentences = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
sentences.append({
|
||||
"solution": next(letters),
|
||||
"sentence": idea["idea"]
|
||||
})
|
||||
|
||||
random.shuffle(sentences)
|
||||
for i, sentence in enumerate(sentences, start=start_id):
|
||||
sentence["id"] = i
|
||||
return sentences
|
||||
|
||||
@staticmethod
|
||||
def randomize_mc_options_order(questions):
|
||||
option_ids = ['A', 'B', 'C', 'D']
|
||||
|
||||
for question in questions:
|
||||
# Store the original solution text
|
||||
original_solution_text = next(
|
||||
option['text'] for option in question['options'] if option['id'] == question['solution'])
|
||||
|
||||
# Shuffle the options
|
||||
random.shuffle(question['options'])
|
||||
|
||||
# Update the option ids and find the new solution id
|
||||
for idx, option in enumerate(question['options']):
|
||||
option['id'] = option_ids[idx]
|
||||
if option['text'] == original_solution_text:
|
||||
question['solution'] = option['id']
|
||||
|
||||
return questions
|
||||
125
ielts_be/helpers/file.py
Normal file
125
ielts_be/helpers/file.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import aiofiles
|
||||
import numpy as np
|
||||
import pypandoc
|
||||
from PIL import Image
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
class FileHelper:
|
||||
|
||||
@staticmethod
|
||||
def delete_files_older_than_one_day(directory: str):
|
||||
current_time = datetime.datetime.now()
|
||||
|
||||
for entry in os.scandir(directory):
|
||||
if entry.is_file():
|
||||
file_path = Path(entry)
|
||||
file_name = file_path.name
|
||||
file_modified_time = datetime.datetime.fromtimestamp(file_path.stat().st_mtime)
|
||||
time_difference = current_time - file_modified_time
|
||||
if time_difference.days > 1 and "placeholder" not in file_name:
|
||||
file_path.unlink()
|
||||
print(f"Deleted file: {file_path}")
|
||||
|
||||
# Supposedly pandoc covers a wide range of file extensions only tested with docx
|
||||
@staticmethod
|
||||
def convert_file_to_pdf(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'pdf', outputfile=output_path, extra_args=[
|
||||
'-V', 'geometry:paperwidth=5.5in',
|
||||
'-V', 'geometry:paperheight=8.5in',
|
||||
'-V', 'geometry:margin=0.5in',
|
||||
'-V', 'pagestyle=empty'
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def convert_file_to_html(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'html', outputfile=output_path)
|
||||
|
||||
@staticmethod
|
||||
def pdf_to_png(path_id: str):
|
||||
to_png = f"pdftoppm -png exercises.pdf page"
|
||||
result = subprocess.run(to_png, shell=True, cwd=f'./tmp/{path_id}', capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(
|
||||
f"Couldn't convert pdf to png. Failed to run command '{to_png}' -> ```cmd {result.stderr}```")
|
||||
|
||||
@staticmethod
|
||||
def is_page_blank(image_bytes: bytes, image_threshold=10) -> bool:
|
||||
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||
img_gray = img.convert('L')
|
||||
img_array = np.array(img_gray)
|
||||
non_white_pixels = np.sum(img_array < 255)
|
||||
|
||||
return non_white_pixels <= image_threshold
|
||||
|
||||
@classmethod
|
||||
async def _encode_image(cls, image_path: str, image_threshold=10) -> Optional[str]:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
image_bytes = await image_file.read()
|
||||
|
||||
if cls.is_page_blank(image_bytes, image_threshold):
|
||||
return None
|
||||
|
||||
return base64.b64encode(image_bytes).decode('utf-8')
|
||||
|
||||
@classmethod
|
||||
async def b64_pngs(cls, path_id: str, files: list[str]):
|
||||
png_messages = []
|
||||
for filename in files:
|
||||
b64_string = await cls._encode_image(os.path.join(f'./tmp/{path_id}', filename))
|
||||
if b64_string:
|
||||
png_messages.append({
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/png;base64,{b64_string}"
|
||||
}
|
||||
})
|
||||
return png_messages
|
||||
|
||||
@staticmethod
|
||||
def remove_directory(path):
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while trying to remove {path}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def remove_file(file_path):
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while trying to remove the file {file_path}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
async def save_upload(file: UploadFile, name: str = "upload", path_id: str = None) -> Tuple[str, str]:
|
||||
ext = file.filename.split('.')[-1]
|
||||
path_id = str(uuid.uuid4()) if path_id is None else path_id
|
||||
os.makedirs(f'./tmp/{path_id}', exist_ok=True)
|
||||
|
||||
tmp_filename = f'./tmp/{path_id}/{name}.{ext}'
|
||||
file_bytes: bytes = await file.read()
|
||||
|
||||
async with aiofiles.open(tmp_filename, 'wb') as file:
|
||||
await file.write(file_bytes)
|
||||
|
||||
return ext, path_id
|
||||
|
||||
@staticmethod
|
||||
async def encode_image(image_path: str) -> str:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
img = await image_file.read()
|
||||
return base64.b64encode(img).decode('utf-8')
|
||||
28
ielts_be/helpers/text.py
Normal file
28
ielts_be/helpers/text.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from nltk.corpus import words
|
||||
|
||||
|
||||
class TextHelper:
|
||||
|
||||
@classmethod
|
||||
def has_words(cls, text: str):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
return any(word.lower() in english_words for word in words_in_input)
|
||||
|
||||
@classmethod
|
||||
def has_x_words(cls, text: str, quantity):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= quantity
|
||||
|
||||
@staticmethod
|
||||
def _has_common_words(text: str):
|
||||
english_words = {"the", "be", "to", "of", "and", "a", "in", "that", "have", "i"}
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= 10
|
||||
@@ -1,89 +1,89 @@
|
||||
# This is a work in progress. There are still bugs. Once it is production-ready this will become a full repo.
|
||||
import os
|
||||
|
||||
|
||||
def count_tokens(text, model_name="gpt-3.5-turbo", debug=False):
|
||||
"""
|
||||
Count the number of tokens in a given text string without using the OpenAI API.
|
||||
|
||||
This function tries three methods in the following order:
|
||||
1. tiktoken (preferred): Accurate token counting similar to the OpenAI API.
|
||||
2. nltk: Token counting using the Natural Language Toolkit library.
|
||||
3. split: Simple whitespace-based token counting as a fallback.
|
||||
|
||||
Usage:
|
||||
------
|
||||
text = "Your text here"
|
||||
result = count_tokens(text, model_name="gpt-3.5-turbo", debug=True)
|
||||
print(result)
|
||||
|
||||
Required libraries:
|
||||
-------------------
|
||||
- tiktoken: Install with 'pip install tiktoken'
|
||||
- nltk: Install with 'pip install nltk'
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
text : str
|
||||
The text string for which you want to count tokens.
|
||||
model_name : str, optional
|
||||
The OpenAI model for which you want to count tokens (default: "gpt-3.5-turbo").
|
||||
debug : bool, optional
|
||||
Set to True to print error messages (default: False).
|
||||
|
||||
Returns:
|
||||
--------
|
||||
result : dict
|
||||
A dictionary containing the number of tokens and the method used for counting.
|
||||
"""
|
||||
|
||||
# Try using tiktoken
|
||||
try:
|
||||
import tiktoken
|
||||
encoding = tiktoken.encoding_for_model(model_name)
|
||||
num_tokens = len(encoding.encode(text))
|
||||
result = {"n_tokens": num_tokens, "method": "tiktoken"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using tiktoken: {e}")
|
||||
pass
|
||||
|
||||
# Try using nltk
|
||||
try:
|
||||
import nltk
|
||||
nltk.download("punkt")
|
||||
tokens = nltk.word_tokenize(text)
|
||||
result = {"n_tokens": len(tokens), "method": "nltk"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using nltk: {e}")
|
||||
pass
|
||||
|
||||
# If nltk and tiktoken fail, use a simple split-based method
|
||||
tokens = text.split()
|
||||
result = {"n_tokens": len(tokens), "method": "split"}
|
||||
return result
|
||||
|
||||
|
||||
class TokenBuffer:
|
||||
def __init__(self, max_tokens=2048):
|
||||
self.max_tokens = max_tokens
|
||||
self.buffer = ""
|
||||
self.token_lengths = []
|
||||
self.token_count = 0
|
||||
|
||||
def update(self, text, model_name="gpt-3.5-turbo", debug=False):
|
||||
new_tokens = count_tokens(text, model_name=model_name, debug=debug)["n_tokens"]
|
||||
self.token_count += new_tokens
|
||||
self.buffer += text
|
||||
self.token_lengths.append(new_tokens)
|
||||
|
||||
while self.token_count > self.max_tokens:
|
||||
removed_tokens = self.token_lengths.pop(0)
|
||||
self.token_count -= removed_tokens
|
||||
self.buffer = self.buffer.split(" ", removed_tokens)[-1]
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
# This is a work in progress. There are still bugs. Once it is production-ready this will become a full repo.
|
||||
|
||||
import tiktoken
|
||||
import nltk
|
||||
|
||||
|
||||
def count_tokens(text, model_name="gpt-3.5-turbo", debug=False):
|
||||
"""
|
||||
Count the number of tokens in a given text string without using the OpenAI API.
|
||||
|
||||
This function tries three methods in the following order:
|
||||
1. tiktoken (preferred): Accurate token counting similar to the OpenAI API.
|
||||
2. nltk: Token counting using the Natural Language Toolkit library.
|
||||
3. split: Simple whitespace-based token counting as a fallback.
|
||||
|
||||
Usage:
|
||||
------
|
||||
text = "Your text here"
|
||||
result = count_tokens(text, model_name="gpt-3.5-turbo", debug=True)
|
||||
print(result)
|
||||
|
||||
Required libraries:
|
||||
-------------------
|
||||
- tiktoken: Install with 'pip install tiktoken'
|
||||
- nltk: Install with 'pip install nltk'
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
text : str
|
||||
The text string for which you want to count tokens.
|
||||
model_name : str, optional
|
||||
The OpenAI model for which you want to count tokens (default: "gpt-3.5-turbo").
|
||||
debug : bool, optional
|
||||
Set to True to print error messages (default: False).
|
||||
|
||||
Returns:
|
||||
--------
|
||||
result : dict
|
||||
A dictionary containing the number of tokens and the method used for counting.
|
||||
"""
|
||||
|
||||
# Try using tiktoken
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model_name)
|
||||
num_tokens = len(encoding.encode(text))
|
||||
result = {"n_tokens": num_tokens, "method": "tiktoken"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using tiktoken: {e}")
|
||||
pass
|
||||
|
||||
# Try using nltk
|
||||
try:
|
||||
# Passed nltk.download("punkt") to server.py's @asynccontextmanager
|
||||
tokens = nltk.word_tokenize(text)
|
||||
result = {"n_tokens": len(tokens), "method": "nltk"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using nltk: {e}")
|
||||
pass
|
||||
|
||||
# If nltk and tiktoken fail, use a simple split-based method
|
||||
tokens = text.split()
|
||||
result = {"n_tokens": len(tokens), "method": "split"}
|
||||
return result
|
||||
|
||||
|
||||
class TokenBuffer:
|
||||
def __init__(self, max_tokens=2048):
|
||||
self.max_tokens = max_tokens
|
||||
self.buffer = ""
|
||||
self.token_lengths = []
|
||||
self.token_count = 0
|
||||
|
||||
def update(self, text, model_name="gpt-3.5-turbo", debug=False):
|
||||
new_tokens = count_tokens(text, model_name=model_name, debug=debug)["n_tokens"]
|
||||
self.token_count += new_tokens
|
||||
self.buffer += text
|
||||
self.token_lengths.append(new_tokens)
|
||||
|
||||
while self.token_count > self.max_tokens:
|
||||
removed_tokens = self.token_lengths.pop(0)
|
||||
self.token_count -= removed_tokens
|
||||
self.buffer = self.buffer.split(" ", removed_tokens)[-1]
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
5
ielts_be/mappers/__init__.py
Normal file
5
ielts_be/mappers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .level import LevelMapper
|
||||
|
||||
__all__ = [
|
||||
"LevelMapper"
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user