diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..02149d8d7ebb47c61087c1e03a954549f856f573 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 3478 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/Longformer_checkpoint/__init__.py b/Longformer_checkpoint/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Longformer_checkpoint/config.json b/Longformer_checkpoint/config.json new file mode 100644 index 0000000000000000000000000000000000000000..1dc68ac464a6e97e78e6ebe6c0f1ff4dce01750d --- /dev/null +++ b/Longformer_checkpoint/config.json @@ -0,0 +1,56 @@ +{ + "architectures": [ + "CustomLongformerForSequenceClassification" + ], + "attention_mode": "longformer", + "attention_probs_dropout_prob": 0.1, + "attention_window": [ + 512, + 512, + 512, + 512, + 512, + 512, + 512, + 512, + 512, + 512, + 512, + 512 + ], + "bos_token_id": 0, + "eos_token_id": 2, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout_prob": 0.1, + "hidden_size": 768, + "id2label": { + "0": "LABEL_0", + "1": "LABEL_1", + "2": "LABEL_2", + "3": "LABEL_3" + }, + "ignore_attention_mask": false, + "initializer_range": 0.02, + "intermediate_size": 3072, + "label2id": { + "LABEL_0": 0, + "LABEL_1": 1, + "LABEL_2": 2, + "LABEL_3": 3 + }, + "layer_norm_eps": 1e-05, + "max_position_embeddings": 4098, + "model_type": "longformer", + "num_attention_heads": 12, + "num_hidden_layers": 12, + "onnx_export": false, + "output_hidden_states": true, + "pad_token_id": 1, + "problem_type": "regression", + "sep_token_id": 2, + "torch_dtype": "float32", + "transformers_version": "4.51.3", + "type_vocab_size": 1, + "vocab_size": 50265 +} diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..df579532508e823ddf366a11e9d74cdfd62a9273 --- /dev/null +++ b/app.py @@ -0,0 +1,6 @@ +from my_app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True,host='0.0.0.0') \ No newline at end of file diff --git a/configs.py b/configs.py new file mode 100644 index 0000000000000000000000000000000000000000..49c0e8ad73c9c91170696e87e8d7d97d813d4616 --- /dev/null +++ b/configs.py @@ -0,0 +1,3 @@ +SECRET_KEY = 'secret_key_here' +SQLALCHEMY_DATABASE_URI = 'sqlite:///users.db' +SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/data/dataset.py b/data/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..42d3e2993925fad13ef8b26b8f52dd73351f7ef3 --- /dev/null +++ b/data/dataset.py @@ -0,0 +1,26 @@ +import torch +class EssayDataset(torch.utils.data.Dataset): + def __init__(self, dataframe, tokenizer, max_length): + self.data = dataframe + self.tokenizer = tokenizer + self.max_length = max_length + + def __len__(self): + return len(self.data) + + def __getitem__(self, idx): + text = self.data.iloc[idx]['train_input'] + labels = self.data.iloc[idx]['labels'] + encoding = self.tokenizer( + text, + max_length=self.max_length, + padding='max_length', + truncation=True, + return_tensors='pt' + ) + return { + 'input_ids': encoding['input_ids'].flatten(), + 'attention_mask': encoding['attention_mask'].flatten(), + 'labels': torch.tensor(labels, dtype=torch.float) + } + diff --git a/inference/__init__.py b/inference/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/inference/__pycache__/__init__.cpython-312.pyc b/inference/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60c77ea0daea8c7f95da99143802c171b4355520 Binary files /dev/null and b/inference/__pycache__/__init__.cpython-312.pyc differ diff --git a/inference/__pycache__/infer_single.cpython-312.pyc b/inference/__pycache__/infer_single.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..878aedb2a957355db3c039994851b3d805a2a04f Binary files /dev/null and b/inference/__pycache__/infer_single.cpython-312.pyc differ diff --git a/inference/infer_single.py b/inference/infer_single.py new file mode 100644 index 0000000000000000000000000000000000000000..b44d503657c1ac540284e240e0eabc370b7f322b --- /dev/null +++ b/inference/infer_single.py @@ -0,0 +1,85 @@ +from utils.data_utils import * +from utils.prompts import * +import torch +from my_app import * + +def replace_single_newlines(text): + return re.sub(r'(? x + 1); // Array from 1 to the length of scores + + const rubricChartData = { + labels: essayNumbers, + datasets: [ + { + label: 'Task Response', + data: scoresTR, + borderColor: 'rgba(54, 162, 235, 1.0)', + backgroundColor: 'rgba(54, 162, 235, 0.6)', + fill: false, + pointRadius: 5 + }, + { + label: 'Coherence and Cohesion', + data: scoresCC, + borderColor: 'rgba(255, 99, 132, 1.0)', + backgroundColor: 'rgba(255, 99, 132, 0.6)', + fill: false, + pointRadius: 5 + }, + { + label: 'Lexical Resource', + data: scoresLR, + borderColor: 'rgba(75, 192, 192, 1.0)', + backgroundColor: 'rgba(75, 192, 192, 0.6)', + fill: false, + pointRadius: 5 + }, + { + label: 'Grammatical Range and Accuracy', + data: scoresGRA, + borderColor: 'rgba(153, 102, 255, 1.0)', + backgroundColor: 'rgba(153, 102, 255, 0.6)', + fill: false, + pointRadius: 5 + } + ] + }; + + const rubricChartOptions = { + responsive: true, + plugins: { + tooltip: { + callbacks: { + label: function (tooltipItem) { + const index = tooltipItem.dataIndex; + switch (tooltipItem.dataset.label) { + case 'Task Response': + return `Task Response Score: ${tooltipItem.raw}, Time: ${creationTimes[index]}`; + case 'Coherence and Cohesion': + return `Coherence and Cohesion Score: ${tooltipItem.raw}, Time: ${creationTimes[index]}`; + case 'Lexical Resource': + return `Lexical Resource Score: ${tooltipItem.raw}, Time: ${creationTimes[index]}`; + case 'Grammatical Range and Accuracy': + return `Grammatical Range and Accuracy Score: ${tooltipItem.raw}, Time: ${creationTimes[index]}`; + default: + return `Score: ${tooltipItem.raw}, Time: ${creationTimes[index]}`; + } + } + } + }, + legend: { + display: true, + labels: { + color: 'white', + padding: 20 + }, + position: 'top', + align: 'center' + } + }, + scales: { + x: { + title: { + display: true, + text: 'Essay Number', + color: 'white' + }, + ticks: { + color: 'white' + }, + grid: { + display: true + } + }, + y: { + title: { + display: true, + text: 'Score', + color: 'white' + }, + ticks: { + color: 'white' + }, + min: 0, + max: 10, + grid: { + display: true, + color: 'rgba(255, 255, 255, 0.2)' + } + } + } + }; + + new Chart(document.getElementById('RubricChart'), { + type: 'line', + data: rubricChartData, + options: rubricChartOptions + }); + + const essaysPerDay = {}; + creationTimes.forEach(time => { + const date = time.split(' ')[0]; + essaysPerDay[date] = (essaysPerDay[date] || 0) + 1; + }); + const sortedEssaysArray = Object.entries(essaysPerDay).sort((a, b) => new Date(a[0]) - new Date(b[0])); + const sortedEssaysPerDay = Object.fromEntries(sortedEssaysArray); + + const dates = Object.keys(sortedEssaysPerDay); + const essayCounts = Object.values(sortedEssaysPerDay); + + const essayCountChartData = { + labels: dates, + datasets: [{ + label: 'Essays Per Day', + data: essayCounts, + borderColor: 'rgba(255, 165, 0, 1.0)', + backgroundColor: 'rgba(145, 128, 128, 0.5)', + fill: true, + pointRadius: 5 + }] + }; + + const essayCountChartOptions = { + responsive: true, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + title: { + display: true, + text: 'Date', + color: 'white' + }, + ticks: { + color: 'white' + }, + grid: { + display: true + } + }, + y: { + title: { + display: true, + text: 'Number of Essays', + color: 'white' + }, + ticks: { + color: 'white' + }, + grid: { + display: true, + color: 'rgba(255, 255, 255, 0.2)' // Light grid lines + } + } + } + }; + new Chart(document.getElementById('EssayCountChart'), { + type: 'bar', + data: essayCountChartData, + options: essayCountChartOptions + }); +} diff --git a/my_app/static/js/home_page.js b/my_app/static/js/home_page.js new file mode 100644 index 0000000000000000000000000000000000000000..63b6355939d9dabca8c542fd19d1417d3c49b870 --- /dev/null +++ b/my_app/static/js/home_page.js @@ -0,0 +1,65 @@ +$(document).ready(function() { + $('#toggle-sidebar').on('click', function() { + const sidebar = $('#sidebar'); + sidebar.toggleClass('active'); + + if (sidebar.hasClass('active')) { + $(this).text("✖"); + } else { + $(this).text("☰"); + } + }); + + $('#close-sidebar').on('click', function() { + const sidebar = $('#sidebar'); + sidebar.removeClass('active'); + $('#toggle-sidebar').text("☰"); + }); + + var isEmpty = $('#isempty').val() === 'True'; + if (isEmpty) { + $('#alertModal').modal('show'); + } + + $('#essay').on('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + var start = this.selectionStart; + var end = this.selectionEnd; + var text = this.value; + this.value = text.substring(0, start) + "\n" + text.substring(end); + this.selectionStart = this.selectionEnd = start + 1; + } + }); + + let countdownSeconds; + const countdownDisplay = document.getElementById("countdown-timer"); + let countdownInterval; + + function updateCountdown() { + const minutes = Math.floor(countdownSeconds / 60); + const seconds = countdownSeconds % 60; + countdownDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + countdownSeconds--; + if (countdownSeconds >= 0) { + countdownInterval = setTimeout(updateCountdown, 1000); + } else { + countdownDisplay.textContent = "Time's up!"; + } + } + + $('#start-countdown').on('click', function() { + const time = countdownDisplay.textContent.trim(); + const timeParts = time.split(':'); + const minutesInput = parseInt(timeParts[0], 10) || 0; + const secondsInput = parseInt(timeParts[1], 10) || 0; + countdownSeconds = minutesInput * 60 + secondsInput; + + clearTimeout(countdownInterval); + updateCountdown(); + }); + + $('#stop-countdown').on('click', function() { + clearTimeout(countdownInterval); + }); +}); diff --git a/my_app/static/js/particles_init.js b/my_app/static/js/particles_init.js new file mode 100644 index 0000000000000000000000000000000000000000..13e51c6e648e12937beb99378823be5050123003 --- /dev/null +++ b/my_app/static/js/particles_init.js @@ -0,0 +1,55 @@ +document.addEventListener("DOMContentLoaded", function () { + particlesJS("particles-js", { + "particles": { + "number": { + "value": 380, + "density": { + "enable": true, + "value_area": 800 + } + }, + "color": { + "value": "#b3b3cc" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + } + }, + "opacity": { + "value": 0.5, + "random": false + }, + "size": { + "value": 3, + "random": true + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#b3b3cc", + "opacity": 0.4, + "width": 1 + }, + "move": { + "enable": true, + "speed": 1 + } + }, + "interactivity": { + "events": { + "onhover": { + "enable": true, + "mode": "grab" + }, + "onclick": { + "enable": true, + "mode": "push" + } + } + }, + "retina_detect": true + }); +}); diff --git a/my_app/static/styles.css b/my_app/static/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..40b4deac8d59780197d2a8255e64d85177950b05 --- /dev/null +++ b/my_app/static/styles.css @@ -0,0 +1,347 @@ +:root { + --background-color: rgb(46, 44, 46); + --navbar-color: rgb(52, 45, 53, 0.0); + --footer-background-color: var(--navbar-color); + --footer-text-color: rgb(145, 128, 128); + --title-color: #d6cdcd; + --button-bg-color: #505050; + --button-hover-bg-color: #404040; + --button-border-color: #404040; + --button-hover-border-color: #303030; + --input-bg-color: rgba(255, 255, 255, 0.5); + --input-text-color: #d6cdcd; + --alert-color: var(--input-text-color); + --toggle-color: #45586d; + --link-color: var(--title-color); + --link-hover-color: var(--title-color); + --modal-background-color: var(--background-color); + --modal-text-color: var(--title-color); +} + +#particles-js { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--background-color); + background-image: url(""); + background-repeat: no-repeat; + background-size: cover; + background-position: 50% 50%; + z-index: -1; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + display: flex; + flex-direction: column; + min-height: 100vh; + color: var(--title-color); + font-family: 'Georgia', serif; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; +} + +input[type="text"], input[type="email"], input[type="password"] { + background-color: rgba(10, 9, 9, 0.5); + border: 1px solid #100f0f; + color: white; + border-radius: 4px; + padding: 10px; +} + +textarea { + background-color:transparent; + background: transparent; + resize: vertical; + overflow-y: auto; +} + +.modal-content { + background-color: var(--modal-background-color); + color: var(--modal-background-color); + z-index: 1050; +} + +.modal-header .close { + color: var(--modal-text-color); +} + +body, h1, h2, h3, p, a { + font-family: 'Georgia', serif; + text-align: center; +} + +table { + margin-top: 20px; + color: var(--title-color); +} + +thead { + color: var(--title-color); +} + +tbody { + color: var(--title-color); +} + +.footer { + background-color: var(--footer-background-color); + color: var(--footer-text-color); + padding: 10px; + text-align: center; + margin-top: 20px; + position: relative; + bottom: 0; + width: 100%; +} + +.navbar { + margin-bottom: 20px; + background-color: var(--navbar-color); +} + +a.nav-link { + color: var(--link-color); + text-decoration: none; +} + +a.nav-link:hover { + color: var(--link-hover-color); +} + +.center-text { + text-align: center; +} + +.btn-center { + display: flex; + justify-content: center; +} + +.footer a { + color: var(--link-color); + text-decoration: none; +} + +.footer a:hover { + color: var(--link-hover-color); +} + +textarea { + background-color: #1e0606 ; + border: 1px solid #ccc; + resize: vertical; + overflow-y: auto; +} + +.btn-primary { + background-color: var(--button-bg-color); + border-color: var(--button-border-color); + margin-bottom: 20px; +} + +.btn-primary:hover { + background-color: var(--button-hover-bg-color); + border-color: var(--button-hover-border-color); +} + +h1, h2 { + color: var(--title-color); + padding-top: 20px; +} + +.table td, .table th { + text-align: left; + vertical-align: middle; +} + +.text-center { + margin-top: 20px; +} + +.btn-danger { + width: 100px; +} + +.btn-info { + width: 100px; + height: 50; +} + +.form-text.text-muted { + color: var(--alert-color) !important; +} + +.btn-toggle { + border: none; + background: none; + color: var(--toggle-color); + cursor: pointer; +} + +.content-hidden { + display: none; +} + +.container { + flex: 1; +} + +.modal-content { + background-color: var(--modal-background-color); + color: var(--modal-text-color); +} + +.modal-header .close { + color: var(--modal-text-color); +} + +.flip-card { + background-color: transparent; + width: 100%; + height: 200px; + perspective: 1000px; +} + +.flip-card-inner { + position: relative; + width: 100%; + height: 100%; + text-align: center; + transition: transform 0.8s; + transform-style: preserve-3d; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); +} + +.flip-card:hover .flip-card-inner { + transform: rotateY(180deg); +} + +.flip-card-front, +.flip-card-back { + position: absolute; + width: 100%; + height: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; +} + +.flip-card-front { + background-color: #313840; + color: white; +} + +.flip-card-back { + background-color: #8fa2b5; + color: black; + transform: rotateY(180deg); +} + + +.sidebar { + position: fixed; + top: 0; + left: -300px; + width: 300px; + height: 100%; + background-color: #343a40; + color: white; + transition: 0.3s; + z-index: 1000; +} + +.sidebar.active { + left: 0; +} + +.sidebar-content { + padding: 20px; +} + +.flashcards { + display: flex; + flex-wrap: wrap; + justify-content: space-around; +} + +.flashcard { + background-color: white; + width: 900px; + height: 900px; + margin: 20px; + perspective: 1000px; +} + +.flashcard-inner { + position: relative; + width: 100%; + height: 100%; + text-align: center; + transition: transform 0.8s; + transform-style: preserve-3d; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); + border-radius: 10px; +} + +.flashcard:hover .flashcard-inner { + transform: rotateY(180deg); +} + +.flashcard-front, .flashcard-back { + position: absolute; + width: 100%; + height: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + border-radius: 10px; +} + +.flashcard-front { + background-color: #091d2b; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.flashcard-back { + background-color: rgb(207, 214, 234); + color: black; + transform: rotateY(180deg); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + box-sizing: border-box; + text-align: justify; +} + +.col-lg-7 { + background-color: rgba(68, 68, 68, 0.85); + padding: 20px; + border-radius: 10px; + color: white; +} + +.col-lg-7 h5 { + font-size: 1.2rem; + margin-top: 15px; + color: #f8f9fa; +} + +.col-lg-7 p { + font-size: 1rem; + line-height: 1.6; +} diff --git a/my_app/templates/403.html b/my_app/templates/403.html new file mode 100644 index 0000000000000000000000000000000000000000..b09ee465ea658c69818d05d08b29d28b1ddd903a --- /dev/null +++ b/my_app/templates/403.html @@ -0,0 +1,14 @@ + + + + Access Denied + + + +
+

Access Denied

+

You do not have permission to access this page.

+ Go Home +
+ + diff --git a/my_app/templates/about_us.html b/my_app/templates/about_us.html new file mode 100644 index 0000000000000000000000000000000000000000..313d9c25398fd48ef4dacaac3042cc3fc1586ab7 --- /dev/null +++ b/my_app/templates/about_us.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} + +{% block content %} +
+

About Us

+

+ Welcome to the project page for SimpleAES, a comprehensive system designed to evaluate English + essays based on the IELTS rubrics. The primary goal of this project is to develop an automated tool that + can accurately score essays on four critical criteria: Task Response, Coherence and Cohesion, + Lexical Resource, and Grammatical Range and Accuracy. This project aims to provide a reliable + and objective means of essay assessment, helping both learning and evaluation processes in educational + settings. +

+
+

+ The IELTS (International English Language Testing System) is a widely recognized standard for + assessing English language proficiency. It is used by educational institutions, employers, + and immigration authorities worldwide. To meet the specified standards set by IELTS, this project + leverages advanced natural language processing (NLP) techniques and neural network to evaluate essays + with a high degree of precision and consistency. +

+
+

+ The project focuses on the following four criteria: +

+ +
+

+ By implementing this automated essay scoring system, the project aims to provide students + and educators with a powerful tool for improving writing skills and ensuring fair and consistent + evaluation. The system can be used for practice, feedback, and formal assessment, helping learners to + identify areas for improvement and track their progress over time. Ultimately, the project seeks to + enhance the teaching and learning of English writing by leveraging technology to provide high-quality, + accessible, and efficient essay evaluations. +

+
+{% endblock %} diff --git a/my_app/templates/base.html b/my_app/templates/base.html new file mode 100644 index 0000000000000000000000000000000000000000..cf04971eb2f68f91e8042ec96a7c878a66687097 --- /dev/null +++ b/my_app/templates/base.html @@ -0,0 +1,75 @@ + + + + {% block title %}{% endblock %} + + + + + + + + +
+
+ + +
+ {% block content %} + {% endblock %} +
+ + + + + + + + + + + {% block extra_scripts %}{% endblock %} +
+ + diff --git a/my_app/templates/coherence_cohesion.html b/my_app/templates/coherence_cohesion.html new file mode 100644 index 0000000000000000000000000000000000000000..0c9395d68380c4e08c79d8f8f31ee5e851e87112 --- /dev/null +++ b/my_app/templates/coherence_cohesion.html @@ -0,0 +1,106 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Coherence & Cohesion

+
+
+
+
+ Band 9 +
+
+ The message can be followed effortlessly. Cohesion is used in such a way that it very rarely attracts attention. Any lapses in coherence or cohesion are minimal. Paragraphing is skilfully managed. +
+
+
+
+
+
+ Band 8 +
+
+ The message can be followed with ease. Information and ideas are logically sequenced, and cohesion is well managed. Occasional lapses in coherence and cohesion may occur. Paragraphing is used sufficiently and appropriately. +
+
+
+
+
+
+ Band 7 +
+
+ Information and ideas are logically organised, and there is a clear progression throughout the response. + A range of cohesive devices including reference and substitution is used flexibly but with some inaccuracies or some over/under use. + Paragraphing is generally used effectively to support overall coherence, and the sequencing of ideas within a paragraph is generally logical. +
+
+
+
+
+
+ Band 6 +
+
+ Information and ideas are generally arranged coherently and there is a clear overall progression. Cohesive devices are used to some good effect but cohesion within and/or between sentences may be faulty or mechanical due to misuse, overuse or omission. The use of reference and substitution may lack flexibility or clarity and result in some repetition or error. Paragraphing may not always be logical and/or the central topic may not always be clear. +
+
+
+
+
+
+ Band 5 +
+
+ Organisation is evident but is not wholly logical and there may be a lack of overall progression. Nevertheless, there is a sense of underlying coherence to the response. The relationship of ideas can be followed but the sentences are not fluently linked to each other. There may be limited/overuse of cohesive devices with some inaccuracy. The writing may be repetitive due to inadequate and/or inaccurate use of reference and substitution. Paragraphing may be inadequate or missing. +
+
+
+
+
+
+ Band 4 +
+
+ Information and ideas are evident but not arranged coherently and there is no clear progression within the response. Relationships between ideas can be unclear and/or inadequately marked. There is some use of basic cohesive devices, which may be inaccurate or repetitive. There is inaccurate use or a lack of substitution or referencing. There may be no paragraphing and/or no clear main topic within paragraphs. +
+
+
+
+
+
+ Band 3 +
+
+ There is no apparent logical organisation. Ideas are discernible but difficult to relate to each other. There is minimal use of sequencers or cohesive devices. Those used do not necessarily indicate a logical relationship between ideas. There is difficulty in identifying referencing. Any attempts at paragraphing are unhelpful. +
+
+
+
+
+
+ Band 2 +
+
+ There is little evidence of control of organisational features. +
+
+
+
+
+
+ Band 1 +
+
+ Responses of 20 words or fewer are rated at Band 1. There is no apparent control of organisational features. +
+
+
+
+ Go Back to Rubric Explanation Page +
+{% endblock %} + +{% block extra_css %} + +{% endblock %} diff --git a/my_app/templates/dashboard.html b/my_app/templates/dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..3f9da8d4549f8f4c4e5425e927cea686efcb373c --- /dev/null +++ b/my_app/templates/dashboard.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Dashboard

+
+
+
+

Rubric Score Dynamics

+ +
+
+

Number of Essays Checked per Day

+ +
+
+ + + + + + + + + + + + + + + + {% for history in histories %} + + + + + + + + + + + + {% endfor %} + +
#TimeTopicEssayTask ResponseCoherence and CohesionLexical ResourceGrammatical Range and Accuracy
{{ loop.index }}{{ history.created_at.strftime('%Y-%m-%d %H:%M:%S') }}{{ history.topic | truncate(10) }}{{ history.essay | truncate(100) }}{{ history.score_tr | int }}{{ history.score_cc | int }}{{ history.score_lr | int }}{{ history.score_gra | int }}
+
+ +
+
+ +
+ + + + + + + +{% endblock %} diff --git a/my_app/templates/error.html b/my_app/templates/error.html new file mode 100644 index 0000000000000000000000000000000000000000..c9d88c7d8c7bcef95eda870de1f62e65aa463456 --- /dev/null +++ b/my_app/templates/error.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}Error{% endblock %} + +{% block content %} +
+

Error

+ + Go Back to Home +
+{% endblock %} diff --git a/my_app/templates/grammatical_range_accuracy.html b/my_app/templates/grammatical_range_accuracy.html new file mode 100644 index 0000000000000000000000000000000000000000..c51a6709495fb8032ee63e93c4c27108b1cf12ef --- /dev/null +++ b/my_app/templates/grammatical_range_accuracy.html @@ -0,0 +1,104 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Grammatical Range & Accuracy

+
+
+
+
+ Band 9 +
+
+ A wide range of structures is used with full flexibility and control. Punctuation and grammar are used appropriately throughout. Minor errors are extremely rare and have minimal impact on communication. +
+
+
+
+
+
+ Band 8 +
+
+ A wide range of structures is flexibly and accurately used. The majority of sentences are error-free, and punctuation is well managed. Occasional, non-systematic errors and inappropriacies occur, but have minimal impact on communication. +
+
+
+
+
+
+ Band 7 +
+
+ A variety of complex structures is used with some flexibility and accuracy. Grammar and punctuation are generally well controlled, and error-free sentences are frequent. A few errors in grammar may persist, but these do not impede communication. +
+
+
+
+
+
+ Band 6 +
+
+ A mix of simple and complex sentence forms is used but flexibility is limited. Examples of more complex structures are not marked by the same level of accuracy as in simple structures. Errors in grammar and punctuation occur, but rarely impede communication. +
+
+
+
+
+
+ Band 5 +
+
+ The range of structures is limited and rather repetitive. Although complex sentences are attempted, they tend to be faulty, and the greatest accuracy is achieved on simple sentences. Grammatical errors may be frequent and cause some difficulty for the reader. Punctuation may be faulty. +
+
+
+
+
+
+ Band 4 +
+
+ A very limited range of structures is used. Subordinate clauses are rare and simple sentences predominate. Some structures are produced accurately but grammatical errors are frequent and may impede meaning. Punctuation is often faulty or inadequate. +
+
+
+
+
+
+ Band 3 +
+
+ Sentence forms are attempted, but errors in grammar and punctuation predominate (except in memorised phrases or those taken from the input material). This prevents most meaning from coming through. +
+
+
+
+
+
+ Band 2 +
+
+ There is little or no evidence of sentence forms (except in memorised phrases). +
+
+
+
+
+
+ Band 1 +
+
+ Responses of 20 words or fewer are rated at Band 1. No rateable language is evident. +
+
+
+
+ Go Back to Rubric Explanation Page +
+{% endblock %} + +{% block extra_css %} + +{% endblock %} diff --git a/my_app/templates/index.html b/my_app/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..113718f84214d57955dafb9d758ca3d091e62d7d --- /dev/null +++ b/my_app/templates/index.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} + +{% block title %}Home Page{% endblock %} + +{% block content %} +
+ + + + +
+

Enter Topic and Essay for Prediction

+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + + +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/my_app/templates/infer.html b/my_app/templates/infer.html new file mode 100644 index 0000000000000000000000000000000000000000..cdab3904de85ed09d50d3ec861934a69e1b7c4ce --- /dev/null +++ b/my_app/templates/infer.html @@ -0,0 +1,181 @@ +{% extends 'base.html' %} + +{% block title %}Prediction Results{% endblock %} + +{% block content %} +
+ +
+
+
+
+

Topic

+
+
+

{{ topic }}

+
+
+
+
+ + +
+
+
+
+

Essay Comparison

+
+
+
+ +
+
+
Original Essay
+
+ {{ essay|safe }} +
+
+
+ + +
+
+
Corrected Essay
+
+ {{ corrected_essay|safe }} +
+
+
+
+
+
+
+
+ + +
+ {% set rubrics = [ + ('Task Response', result[0], tr , 'task_response'), + ('Coherence and Cohesion', result[1], cc, 'coherence_cohesion'), + ('Lexical Resource', result[2], lr, 'lexical_resource'), + ('Grammatical Range and Accuracy', result[3], gra, 'grammatical_accuracy') + ] %} + + {% for rubric, score, explanation, link in rubrics %} +
+
+
+
{{ rubric }}
+
+ {{ score|round|int }}/9 +
+

+ {{ explanation }} +

+
+ + + Details + +
+
+
+ + + +
+ {% endfor %} +
+ +
+ Submit Another Essay +
+
+{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/my_app/templates/lexical_resource.html b/my_app/templates/lexical_resource.html new file mode 100644 index 0000000000000000000000000000000000000000..fc322542607eeb9dc22719189f4a243808afdd5e --- /dev/null +++ b/my_app/templates/lexical_resource.html @@ -0,0 +1,104 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Lexical Resource

+
+
+
+
+ Band 9 +
+
+ Full flexibility and precise use are widely evident. A wide range of vocabulary is used accurately and appropriately with very natural and sophisticated control of lexical features. Minor errors in spelling and word formation are extremely rare and have minimal impact on communication. +
+
+
+
+
+
+ Band 8 +
+
+ A wide resource is fluently and flexibly used to convey precise meanings. There is skilful use of uncommon and/or idiomatic items when appropriate, despite occasional inaccuracies in word choice and collocation. Occasional errors in spelling and/or word formation may occur, but have minimal impact on communication. +
+
+
+
+
+
+ Band 7 +
+
+ The resource is sufficient to allow some flexibility and precision. There is some ability to use less common and/or idiomatic items. An awareness of style and collocation is evident, though inappropriacies occur. There are only a few errors in spelling and/or word formation and they do not detract from overall clarity. +
+
+
+
+
+
+ Band 6 +
+
+ The resource is generally adequate and appropriate for the task. The meaning is generally clear in spite of a rather restricted range or a lack of precision in word choice. If the writer is a risk-taker, there will be a wider range of vocabulary used but higher degrees of inaccuracy or inappropriacy. There are some errors in spelling and/or word formation, but these do not impede communication. +
+
+
+
+
+
+ Band 5 +
+
+ The resource is limited but minimally adequate for the task. Simple vocabulary may be used accurately but the range does not permit much variation in expression. There may be frequent lapses in the appropriacy of word choice and a lack of flexibility is apparent in frequent simplifications and/or repetitions. Errors in spelling and/or word formation may be noticeable and may cause some difficulty for the reader. +
+
+
+
+
+
+ Band 4 +
+
+ The resource is limited and inadequate for or unrelated to the task. Vocabulary is basic and may be used repetitively. There may be inappropriate use of lexical chunks (e.g. memorised phrases, formulaic language and/or language from the input material). Inappropriate word choice and/or errors in word formation and/or in spelling may impede meaning. +
+
+
+
+
+
+ Band 3 +
+
+ The resource is inadequate (which may be due to the response being significantly underlength). Possible over-dependence on input material or memorised language. Control of word choice and/or spelling is very limited, and errors predominate. These errors may severely impede meaning. +
+
+
+
+
+
+ Band 2 +
+
+ The resource is extremely limited with few recognisable strings, apart from memorised phrases. There is no apparent control of word formation and/or spelling. +
+
+
+
+
+
+ Band 1 +
+
+ Responses of 20 words or fewer are rated at Band 1. No resource is apparent, except for a few isolated words. +
+
+
+
+ Go Back to Rubric Explanation Page +
+{% endblock %} + +{% block extra_css %} + +{% endblock %} diff --git a/my_app/templates/login.html b/my_app/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..d711f5bcf73dfb2ce417d5006063b567e989cea0 --- /dev/null +++ b/my_app/templates/login.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Login

+
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/my_app/templates/register.html b/my_app/templates/register.html new file mode 100644 index 0000000000000000000000000000000000000000..b8f7b2ad94f0c1b9a32e62663018d52a4fa80e78 --- /dev/null +++ b/my_app/templates/register.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} + +{% block title %}Register{% endblock %} + +{% block content %} +
+

Register

+
+
+ + + Your email here. Example: example@domain.com +
+
+ + + Your nickname here. Example: Nickname1 +
+
+ + + Your password should be more than 8 symbols. +
+
+ + +
+ +
+ +
+
+ + + +
+{% endblock %} +{% block extra_scripts %} + +{% endblock %} diff --git a/my_app/templates/rubric_explanation.html b/my_app/templates/rubric_explanation.html new file mode 100644 index 0000000000000000000000000000000000000000..59915d236b3823084c592ed7971e11352f0dc8e6 --- /dev/null +++ b/my_app/templates/rubric_explanation.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}Rubric Explanation{% endblock %} + +{% block content %} +
+

Explanation

+
+ + +
+
+ + +
+
+ Back +
+
+{% endblock %} diff --git a/my_app/templates/task_response.html b/my_app/templates/task_response.html new file mode 100644 index 0000000000000000000000000000000000000000..91a5472108206d89ef21c22c6eb77f2714cf0930 --- /dev/null +++ b/my_app/templates/task_response.html @@ -0,0 +1,104 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Task Response

+
+
+
+
+ Band 9 +
+
+ The prompt is appropriately addressed and explored in depth. A clear and fully developed position is presented which directly answers the question/s. Ideas are relevant, fully extended and well supported. Any lapses in content or support are extremely rare. +
+
+
+
+
+
+ Band 8 +
+
+ The prompt is appropriately and sufficiently addressed. A clear and well-developed position is presented in response to the question/s. Ideas are relevant, well extended and supported. There may be occasional omissions or lapses in content. +
+
+
+
+
+
+ Band 7 +
+
+ The main parts of the prompt are appropriately addressed. A clear and developed position is presented. Main ideas are extended and supported but there may be a tendency to over-generalise or there may be a lack of focus and precision in supporting ideas/material. +
+
+
+
+
+
+ Band 6 +
+
+ The main parts of the prompt are addressed (though some may be more fully covered than others). An appropriate format is used. A position is presented that is directly relevant to the prompt, although the conclusions drawn may be unclear, unjustified or repetitive. +
+
+
+
+
+
+ Band 5 +
+
+ The main parts of the prompt are incompletely addressed. The format may be inappropriate in places. The writer expresses a position, but the development is not always clear. Some main ideas are put forward, but they are limited and are not sufficiently developed and/or there may be irrelevant detail. +
+
+
+
+
+
+ Band 4 +
+
+ The prompt is tackled in a minimal way, or the answer is tangential, possibly due to some misunderstanding of the prompt. The format may be inappropriate. A position is discernible, but the reader has to read carefully to find it. Main ideas are difficult to identify and such ideas that are identifiable may lack relevance, clarity and/or support. +
+
+
+
+
+
+ Band 3 +
+
+ No part of the prompt is adequately addressed, or the prompt has been misunderstood. No relevant position can be identified, and/or there is little direct response to the question/s. There are few ideas, and these may be irrelevant or insufficiently developed. +
+
+
+
+
+
+ Band 2 +
+
+ The content is barely related to the prompt. No position can be identified. There may be glimpses of one or two ideas without development. There is little relevant message, or the entire response may be off-topic. +
+
+
+
+
+
+ Band 1 +
+
+ Responses of 20 words or fewer are rated at Band 1. The content is wholly unrelated to the prompt. Any copied rubric must be discounted. +
+
+
+
+ Go Back to Rubric Explanation Page +
+{% endblock %} + +{% block extra_css %} + +{% endblock %} diff --git a/my_app/views/__init__.py b/my_app/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8587b918276241f2ebe5c5fe3c4936517abb566c --- /dev/null +++ b/my_app/views/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +auth_bp = Blueprint('auth', __name__, template_folder='templates') +dashboard_bp = Blueprint('dashboard', __name__, template_folder='templates') +infer_bp = Blueprint('infer', __name__, template_folder='templates') +about_bp = Blueprint('about', __name__, template_folder='templates') +error_bp = Blueprint('error', __name__, template_folder='templates') + +from . import auth, dashboard, infer, about, error diff --git a/my_app/views/__pycache__/__init__.cpython-310.pyc b/my_app/views/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96c841b3cbb358ba7251f818020f39746a4eba53 Binary files /dev/null and b/my_app/views/__pycache__/__init__.cpython-310.pyc differ diff --git a/my_app/views/__pycache__/__init__.cpython-311.pyc b/my_app/views/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bc13771b42601fc47cf73b02a9a7ad86fb1d8aa Binary files /dev/null and b/my_app/views/__pycache__/__init__.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/__init__.cpython-312.pyc b/my_app/views/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3943b5b891b375d6924ffce7792c82d56946edc8 Binary files /dev/null and b/my_app/views/__pycache__/__init__.cpython-312.pyc differ diff --git a/my_app/views/__pycache__/about.cpython-311.pyc b/my_app/views/__pycache__/about.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1714f777ea9b483df40d96b2e4693495732d196b Binary files /dev/null and b/my_app/views/__pycache__/about.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/about.cpython-312.pyc b/my_app/views/__pycache__/about.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91cb1033e3eb1a2480b2e4234e18ed15f962017f Binary files /dev/null and b/my_app/views/__pycache__/about.cpython-312.pyc differ diff --git a/my_app/views/__pycache__/auth.cpython-310.pyc b/my_app/views/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b73985f37038780a4c63cc9d4b695c35a72ec29e Binary files /dev/null and b/my_app/views/__pycache__/auth.cpython-310.pyc differ diff --git a/my_app/views/__pycache__/auth.cpython-311.pyc b/my_app/views/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42d48dbef3445dbd16661ae580485ba8677197b2 Binary files /dev/null and b/my_app/views/__pycache__/auth.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/auth.cpython-312.pyc b/my_app/views/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc70fd3eaeb4b6fc377fb294a5d50b0220217132 Binary files /dev/null and b/my_app/views/__pycache__/auth.cpython-312.pyc differ diff --git a/my_app/views/__pycache__/dashboard.cpython-310.pyc b/my_app/views/__pycache__/dashboard.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4f53e2511ccc77800856e1f42f797a8964f7146 Binary files /dev/null and b/my_app/views/__pycache__/dashboard.cpython-310.pyc differ diff --git a/my_app/views/__pycache__/dashboard.cpython-311.pyc b/my_app/views/__pycache__/dashboard.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4eb77a1c47cf0c424b7e5a363e1f4dc03a8d4b6c Binary files /dev/null and b/my_app/views/__pycache__/dashboard.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/dashboard.cpython-312.pyc b/my_app/views/__pycache__/dashboard.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d39b5112709703e68ba959cc0c7f74ec62ca996e Binary files /dev/null and b/my_app/views/__pycache__/dashboard.cpython-312.pyc differ diff --git a/my_app/views/__pycache__/error.cpython-311.pyc b/my_app/views/__pycache__/error.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b238e6dcb4878c153da27a1d6bc2c901de2908d5 Binary files /dev/null and b/my_app/views/__pycache__/error.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/error.cpython-312.pyc b/my_app/views/__pycache__/error.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5b48cb00b35d05e910c59ab5f1bff8e0c1520aa Binary files /dev/null and b/my_app/views/__pycache__/error.cpython-312.pyc differ diff --git a/my_app/views/__pycache__/infer.cpython-310.pyc b/my_app/views/__pycache__/infer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df9d3ec3bd50e0574b3d3abad20cde8b027bc2a3 Binary files /dev/null and b/my_app/views/__pycache__/infer.cpython-310.pyc differ diff --git a/my_app/views/__pycache__/infer.cpython-311.pyc b/my_app/views/__pycache__/infer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..566267407fe7e0a9f3832ff049d9e277bd8dd879 Binary files /dev/null and b/my_app/views/__pycache__/infer.cpython-311.pyc differ diff --git a/my_app/views/__pycache__/infer.cpython-312.pyc b/my_app/views/__pycache__/infer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a47e979f8c9c963859a2b3b9765afe8902b88c1 Binary files /dev/null and b/my_app/views/__pycache__/infer.cpython-312.pyc differ diff --git a/my_app/views/about.py b/my_app/views/about.py new file mode 100644 index 0000000000000000000000000000000000000000..b008edfebd5a2c72f4b86c2ee5c2a248a9193b83 --- /dev/null +++ b/my_app/views/about.py @@ -0,0 +1,6 @@ +from flask import render_template +from . import about_bp + +@about_bp.route('/about_us') +def about_us(): + return render_template('about_us.html') diff --git a/my_app/views/auth.py b/my_app/views/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..203a6412657ef46bd9d4362402fbff59ccf2bc4d --- /dev/null +++ b/my_app/views/auth.py @@ -0,0 +1,50 @@ +from flask import render_template, request, redirect, url_for, flash +from flask_login import login_user, logout_user, login_required, current_user +from . import auth_bp +from ..database import User, db + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + email = request.form['email'] + nickname = request.form['nickname'] + password = request.form['password'] + + existing_email = User.query.filter_by(email=email).first() + existing_nickname = User.query.filter_by(nickname=nickname).first() + + if existing_email: + return render_template('register.html', message="Email already registered.") + if existing_nickname: + return render_template('register.html', message="Nickname already taken.") + + if len(password) < 8: + return render_template('register.html', message="Password must be at least 8 characters long.") + + new_user = User(email=email, nickname=nickname) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return redirect(url_for('auth.login')) + return render_template('register.html') + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + email = request.form['email'] + password = request.form['password'] + user = User.query.filter_by(email=email).first() + if user and user.check_password(password): + login_user(user) + return redirect(url_for('dashboard.dashboard')) + else: + return render_template('login.html', message="Invalid email or password.") + return render_template('login.html') + + +@auth_bp.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('auth.login')) diff --git a/my_app/views/dashboard.py b/my_app/views/dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..9fc7d0e21bbc1153c30cd3d504f614040be47e73 --- /dev/null +++ b/my_app/views/dashboard.py @@ -0,0 +1,38 @@ +from flask import render_template, redirect, url_for, request, flash +from flask_login import login_required, current_user +from . import dashboard_bp +from ..database import History, db + + +@dashboard_bp.route('/dashboard', defaults={'page': 1}) +@dashboard_bp.route('/dashboard/page/') +@login_required +def dashboard(page): + per_page = 10 + pagination = History.query.filter_by(user_id=current_user.id).order_by(History.created_at.desc()).paginate(page=page, per_page=per_page, error_out=False) + histories = pagination.items + + scores_tr = [history.score_tr for history in histories] + scores_cc = [history.score_cc for history in histories] + scores_lr = [history.score_lr for history in histories] + scores_gra = [history.score_gra for history in histories] + creation_times = [history.created_at.strftime('%Y-%m-%d %H:%M:%S') for history in histories] + + return render_template( + 'dashboard.html', + histories=histories, + pagination=pagination, + scores_tr=scores_tr, + scores_cc=scores_cc, + scores_lr=scores_lr, + scores_gra=scores_gra, + creation_times=creation_times + ) +@dashboard_bp.route('/delete_histories', methods=['POST']) +@login_required +def delete_histories(): + history_ids = request.form.getlist('history_ids') + History.query.filter(History.id.in_(history_ids), History.user_id == current_user.id).delete(synchronize_session=False) + db.session.commit() + flash('Selected histories deleted.') + return redirect(url_for('dashboard.dashboard')) diff --git a/my_app/views/error.py b/my_app/views/error.py new file mode 100644 index 0000000000000000000000000000000000000000..4c089ff2e5994f66362be2e5768a0750147638fd --- /dev/null +++ b/my_app/views/error.py @@ -0,0 +1,8 @@ +from . import error_bp +from flask import render_template + +@error_bp.errorhandler(Exception) +def handle_all_exceptions(e): + code = getattr(e, 'code', 500) + error_message = str(e) if hasattr(e, 'description') else "Something went wrong." + return render_template('error.html', code=code, error_message=error_message), code \ No newline at end of file diff --git a/my_app/views/infer.py b/my_app/views/infer.py new file mode 100644 index 0000000000000000000000000000000000000000..41881c062d34c4c69414c31509d7b52e5cc77d73 --- /dev/null +++ b/my_app/views/infer.py @@ -0,0 +1,92 @@ +from flask import render_template, session, request, redirect, url_for, flash +from . import infer_bp +import secrets +from ..database import History, db +from flask_login import current_user +from inference.infer_single import * +from utils.util_func import * +@infer_bp.route('/infer', methods=['GET']) +def infer(): + token = request.args.get('token') + if token is None: + flash("Please go to the home page to check your essay.") + return redirect(url_for('infer.index')) + if token != session.get('inference_token'): + flash("Invalid or expired access token. Please submit the form again.") + return redirect(url_for('infer.index')) + scores = session.get('scores', []) + topic = session.get('topic', "") + essay = session.get('essay', "") + feedback = session.get('feedback', {}) or {} + feedback = dict(feedback) + tr_feedback = feedback.get('TR_feedback', 'No feedback available. Your essay does not make sense, try to refine it.') + cc_feedback = feedback.get('CC_feedback', 'No feedback available. Your essay does not make sense, try to refine it.') + lr_feedback = feedback.get('LR_feedback', 'No feedback available. Your essay does not make sense, try to refine it.') + gra_feedback = feedback.get('GRA_feedback', 'No feedback available. Your essay does not make sense, try to refine it.') + corrected_essay = feedback.get('Corrected_essay', "No suggestions available. Your essay does not make sense, try to refine it.") + essay = essay.replace('\n', '
') + corrected_essay = corrected_essay.replace('\\n\\n', '\\n').replace('\\n', '
') + session.pop('inference_token', None) + return render_template('infer.html', result=scores, topic=topic, essay=essay, tr=tr_feedback, cc=cc_feedback, lr=lr_feedback, gra=gra_feedback, corrected_essay=corrected_essay) + +@infer_bp.route('/', methods=['GET', 'POST']) +def index(): + set_seed(42) + if request.method == 'POST': + topic = request.form['topic'].strip() + essay = request.form['essay'].strip() + if not topic or not essay: + flash('Please provide both a topic and an essay text.') + return render_template('index.html', isempty='True') + num_words = len(essay.split()) + if num_words < 20: + session['scores'] = [1, 1, 1, 1] + session['topic'] = topic + session['essay'] = essay + session['feedback'] = {} + else: + scores, feedback = generate_and_score_essay( + topic, essay + ) + session['scores'] = scores + session['topic'] = topic + session['essay'] = essay.replace('\\n', '\n') + session['feedback'] = feedback or {} # Ensure feedback is at least an empty dict + token = secrets.token_urlsafe(16) + session['inference_token'] = token + if current_user.is_authenticated: + scores = [int(score) for score in session['scores']] + new_history = History( + user_id=current_user.id, + topic=topic, + essay=essay, + score_tr=scores[0], + score_cc=scores[1], + score_lr=scores[2], + score_gra=scores[3] + ) + db.session.add(new_history) + db.session.commit() + + return redirect(url_for('infer.infer', token=token)) + return render_template('index.html', isempty='False', username=current_user.nickname if current_user.is_authenticated else '') + +@infer_bp.route('/rubric_explanation') +def rubric_explanation(): + return render_template('rubric_explanation.html') + +@infer_bp.route('/rubric_explanation/task_response') +def task_response(): + return render_template('task_response.html') + +@infer_bp.route('/rubric_explanation/coherence_cohesion') +def coherence_cohesion(): + return render_template('coherence_cohesion.html') + +@infer_bp.route('/rubric_explanation/lexical_resource') +def lexical_resource(): + return render_template('lexical_resource.html') + +@infer_bp.route('/rubric_explanation/grammatical_range_accuracy') +def grammatical_range_accuracy(): + return render_template('grammatical_range_accuracy.html') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b75c224ed45601c8dfdf0db7de487d8911b58062 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +torch==2.7.0 +transformers==4.52.4 +pandas==2.2.1 +cefrpy +spacy +numpy==1.26.4 +accelerate +optimum==1.26.1 +flask==3.1.1 +flask_login==0.6.3 +werkzeug==3.1.3 +flask_sqlalchemy==3.1.1 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/__pycache__/__init__.cpython-312.pyc b/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e532551ea280e3556812cbf72d725688ab8431d3 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/utils/__pycache__/data_utils.cpython-312.pyc b/utils/__pycache__/data_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a54fc54523480fc90ec5498dad3c62ae15c33a53 Binary files /dev/null and b/utils/__pycache__/data_utils.cpython-312.pyc differ diff --git a/utils/__pycache__/prompts.cpython-312.pyc b/utils/__pycache__/prompts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..201708dfeddcf720f0aeaff257aa3a968e9d9891 Binary files /dev/null and b/utils/__pycache__/prompts.cpython-312.pyc differ diff --git a/utils/__pycache__/util_func.cpython-312.pyc b/utils/__pycache__/util_func.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed4a369980695bccf934d85eb5c954d4b6c2904a Binary files /dev/null and b/utils/__pycache__/util_func.cpython-312.pyc differ diff --git a/utils/data_utils.py b/utils/data_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..276385f2df160d201f070d087386cd296b284a04 --- /dev/null +++ b/utils/data_utils.py @@ -0,0 +1,215 @@ +import re +import pandas as pd +from typing import Dict, List, Union, Tuple +from cefrpy import CEFRSpaCyAnalyzer, CEFRLevel +import spacy + +def extract_feedback_with_clean_quotes(feedback_str: str) -> Dict[str, Union[str, List[str]]]: + section_map = { + "Task Response feedback": "TR_feedback", + "Coherence and Cohesion feedback": "CC_feedback", + "Lexical Resource feedback": "LR_feedback", + "Grammatical Range and Accuracy feedback": "GRA_feedback", + "Is off topic": "is_off_topic", + "Word limit satisfied": "word_limit", + "Corrected essay": "Corrected_essay" + } + + result = {v: None for v in section_map.values()} + quote_results = {f"{v}_quotes": [] for v in section_map.values() if v.endswith('_feedback')} + + section_pattern = r'"(?P
(?:[^"]|\\")+)"\s*:\s*"(?P(?:[^"]|\\")*)"' + + for match in re.finditer(section_pattern, feedback_str): + header = match.group('header') + content = match.group('content').replace('\\"', '"') + + if header in section_map: + key = section_map[header] + result[key] = content + + # Extract and clean quoted phrases for feedback sections + if key.endswith('_feedback'): + quotes = re.findall(r"'(.*?)'", content) + clean_quotes = [] + for quote in quotes: + # Remove trailing punctuation + cleaned = re.sub(r'[.,;:!?]+$', '', quote.strip()) + if cleaned: # Only keep non-empty strings + clean_quotes.append(cleaned) + quote_results[f"{key}_quotes"] = clean_quotes + + # Handle special cases + for orig, new in [("Is off topic", "is_off_topic"), + ("Word limit satisfied", "word_limit")]: + if result[new] is None: + match = re.search(rf'{orig}\s*:\s*"([^"]+)"', feedback_str) + if match: + result[new] = match.group(1) + + # Handle corrected essay (multi-line) + if result["Corrected_essay"] is None: + essay_match = re.search( + r'"Corrected essay"\s*:\s*"(.*?)"(?=\s*[,\]}]|$)', + feedback_str, + re.DOTALL + ) + if essay_match: + result["Corrected_essay"] = essay_match.group(1).replace('\\"', '"') + + return pd.Series({**result, **quote_results}) + + +def extract_feedback_keys_values(feedback_str): + try: + # Map the feedback sections to standardized column names + section_map = { + '"Task Response feedback"': 'TR_feedback', + '"Coherence and Cohesion feedback"': 'CC_feedback', + '"Lexical Resource feedback"': 'LR_feedback', + '"Grammatical Range and Accuracy feedback"': 'GRA_feedback', + '"Corrected essay"': 'Corrected_essay' + } + result = {v: None for v in section_map.values()} # Initialize with None + for original_section, new_key in section_map.items(): + # Find the start of the section + start = feedback_str.find(original_section) + if start == -1: + continue + # Find the end of this section (either next section or end of string) + end = len(feedback_str) + for other_section in section_map: + if other_section != original_section: + other_start = feedback_str.find(other_section, start + 1) + if other_start != -1 and other_start < end: + end = other_start + section_content = feedback_str[start:end].strip() + key_end = section_content.find(':') + if key_end == -1: + continue + value = section_content[key_end+1:].strip().strip(' ,') + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + result[new_key] = value + return pd.Series(result) # Return as Series for DataFrame expansion + except Exception as e: + print(f"Error processing feedback: {e}") + return pd.Series({k: None for k in section_map.values()}) + + +def create_train_input(row): + feedback_parts = [ + f"Task Response Feedback: {row['TR_feedback']}", + f"Coherence and Cohesion Feedback: {row['CC_feedback']}", + f"Lexical Resource Feedback: {row['LR_feedback']}", + f"Grammatical Range and Accuracy Feedback: {row['GRA_feedback']}", + f"The essay has {row['word_count']} words and {row['paragraph_count']} paragraphs.", + f"The CEFR statistics of this essay: {row['cefr_stat']}" + ] + feedback_str = "\n".join(feedback_parts) + + return ( + "{{TOPIC}}\n" + row['topic'] + + "\n{{ESSAY}}\n" + row['essay'] + + "\n{{CORRECTED_ESSAY}}\n" + row['Corrected_essay'] + + "\n{{FEEDBACK}}\n" + feedback_str + ) + +column_mapping = { + 'Task Response': 'TR_score', + 'Coherence and Cohesion': 'CC_score', + 'Lexical Resource': 'LR_score', + 'Grammatical Range and Accuracy': 'GRA_score' +} + + +nlp = spacy.load("en_core_web_sm") + +def get_cefr_stats(text): + if not isinstance(text, str) or not text.strip(): + return {f'{level}_words': 0 for level in ['A1','A2','B1','B2','C1','C2','unknown']} | {'total_words': 0} + + ABBREVIATION_MAPPING = { + "'m": "am", + "'s": "is", + "'re": "are", + "'ve": "have", + "'d": "had", + "n't": "not", + "'ll": "will" + } + + ENTITY_TYPES_TO_SKIP_CEFR = { + 'QUANTITY', 'MONEY', 'LANGUAGE', 'LAW', + 'WORK_OF_ART', 'PRODUCT', 'GPE', + 'ORG', 'FAC', 'PERSON' + } + + def get_word_level_count_statistic(level_tokens: List[Tuple[str, str, bool, float, int, int]]) -> dict: + """Safe counting of CEFR levels with error handling""" + difficulty_levels_count = [0] * 6 + unknown_count = 0 + result = {} + + for token in level_tokens: + try: + level = token[3] + if level is None: + unknown_count += 1 + continue + + # Safely handle level conversion + try: + level_round = round(float(level)) + if 1 <= level_round <= 6: + difficulty_levels_count[level_round - 1] += 1 + else: + unknown_count += 1 + except (ValueError, TypeError): + unknown_count += 1 + + except Exception as e: + print(f"Error processing token: {e}") + unknown_count += 1 + + # Convert to CEFR level names + for i in range(1, 7): + result[f'{CEFRLevel(i)}_words'] = difficulty_levels_count[i - 1] + result['unknown_words'] = unknown_count + result['total_words'] = sum(difficulty_levels_count) + unknown_count + + # Calculate percentages + if result['total_words'] > 0: + for i in range(1, 7): + result[f'{CEFRLevel(i)}_pct'] = (difficulty_levels_count[i - 1] / result['total_words']) * 100 + result['unknown_pct'] = (unknown_count / result['total_words']) * 100 + else: + for i in range(1, 7): + result[f'{CEFRLevel(i)}_pct'] = 0.0 + result['unknown_pct'] = 0.0 + + return result + + try: + # Handle encoding errors by cleaning the text first + clean_text = text.encode('ascii', errors='ignore').decode('ascii') + doc = nlp(clean_text) + text_analyzer = CEFRSpaCyAnalyzer( + entity_types_to_skip=ENTITY_TYPES_TO_SKIP_CEFR, + abbreviation_mapping=ABBREVIATION_MAPPING + ) + tokens = text_analyzer.analize_doc(doc) + ans = str(get_word_level_count_statistic(tokens)) + return ans + + except Exception as e: + print(f"Error analyzing text: {e}") + return str({f'{level}_words': 0 for level in ['A1','A2','B1','B2','C1','C2','unknown']} | {'total_words': 0}) + + + +def replace_single_newlines(text): + # Replace \n not preceded by \n or not followed by \n + return re.sub(r'(?